From 2bd6d544ff458577a0202bcd64ad664d4f1cec08 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Wed, 22 Apr 2026 09:28:47 -0700 Subject: [PATCH 1/8] Bump installer floor to 2026.4.7 (#5134) --- install.ps1 | 10 +++++----- install.sh | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/install.ps1 b/install.ps1 index 3fc9ac4690..7f98fc5cb8 100644 --- a/install.ps1 +++ b/install.ps1 @@ -912,7 +912,7 @@ shell.Run cmd, 0, False if ($SkipTorch) { # No-torch: install unsloth + unsloth-zoo with --no-deps, then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.4.5" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.4.7" unsloth-zoo } if ($baseInstallExit -eq 0) { $NoTorchReq = Find-NoTorchRuntimeFile if ($NoTorchReq) { @@ -920,7 +920,7 @@ shell.Run cmd, 0, False } } } else { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.4.5" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --reinstall-package unsloth --reinstall-package unsloth-zoo "unsloth>=2026.4.7" unsloth-zoo } } if ($baseInstallExit -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red @@ -950,7 +950,7 @@ shell.Run cmd, 0, False if ($SkipTorch) { # No-torch: install unsloth + unsloth-zoo with --no-deps, then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --upgrade-package unsloth --upgrade-package unsloth-zoo "unsloth>=2026.4.5" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --no-deps --upgrade-package unsloth --upgrade-package unsloth-zoo "unsloth>=2026.4.7" unsloth-zoo } if ($baseInstallExit -eq 0) { $NoTorchReq = Find-NoTorchRuntimeFile if ($NoTorchReq) { @@ -958,7 +958,7 @@ shell.Run cmd, 0, False } } } elseif ($StudioLocalInstall) { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.4.5" unsloth-zoo } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.4.7" unsloth-zoo } } else { $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "$PackageName" } } @@ -979,7 +979,7 @@ shell.Run cmd, 0, False # Fallback: GPU detection failed to produce a URL -- let uv resolve torch substep "installing unsloth (this may take a few minutes)..." if ($StudioLocalInstall) { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.4.5" --torch-backend=auto } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.4.7" --torch-backend=auto } if ($baseInstallExit -ne 0) { Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red return diff --git a/install.sh b/install.sh index 6915893ddf..0352887de1 100755 --- a/install.sh +++ b/install.sh @@ -1316,7 +1316,7 @@ if [ "$_MIGRATED" = true ]; then # to prevent transitive torch resolution. run_install_cmd "install unsloth (migrated no-torch)" uv pip install --python "$_VENV_PY" --no-deps \ --reinstall-package unsloth --reinstall-package unsloth-zoo \ - "unsloth>=2026.4.5" unsloth-zoo + "unsloth>=2026.4.7" unsloth-zoo _NO_TORCH_RT="$(_find_no_torch_runtime)" if [ -n "$_NO_TORCH_RT" ]; then run_install_cmd "install no-torch runtime deps" uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT" @@ -1324,7 +1324,7 @@ if [ "$_MIGRATED" = true ]; then else run_install_cmd "install unsloth (migrated)" uv pip install --python "$_VENV_PY" \ --reinstall-package unsloth --reinstall-package unsloth-zoo \ - "unsloth>=2026.4.5" unsloth-zoo + "unsloth>=2026.4.7" unsloth-zoo fi if [ "$STUDIO_LOCAL_INSTALL" = true ]; then substep "overlaying local repo (editable)..." @@ -1487,7 +1487,7 @@ elif [ -n "$TORCH_INDEX_URL" ]; then # runtime deps (typer, safetensors, transformers, etc.) with --no-deps. run_install_cmd "install unsloth (no-torch)" uv pip install --python "$_VENV_PY" --no-deps \ --upgrade-package unsloth --upgrade-package unsloth-zoo \ - "unsloth>=2026.4.5" unsloth-zoo + "unsloth>=2026.4.7" unsloth-zoo _NO_TORCH_RT="$(_find_no_torch_runtime)" if [ -n "$_NO_TORCH_RT" ]; then run_install_cmd "install no-torch runtime deps" uv pip install --python "$_VENV_PY" --no-deps -r "$_NO_TORCH_RT" @@ -1498,7 +1498,7 @@ elif [ -n "$TORCH_INDEX_URL" ]; then fi elif [ "$STUDIO_LOCAL_INSTALL" = true ]; then run_install_cmd "install unsloth (local)" uv pip install --python "$_VENV_PY" \ - --upgrade-package unsloth "unsloth>=2026.4.5" unsloth-zoo + --upgrade-package unsloth "unsloth>=2026.4.7" unsloth-zoo substep "overlaying local repo (editable)..." run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps else @@ -1525,7 +1525,7 @@ else # Fallback: GPU detection failed to produce a URL -- let uv resolve torch substep "installing unsloth (this may take a few minutes)..." if [ "$STUDIO_LOCAL_INSTALL" = true ]; then - run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.4.5" --torch-backend=auto + run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.4.7" --torch-backend=auto substep "overlaying local repo (editable)..." run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps else From a1fb7c1297bd534df7bd7a58672f19cbe05edc6d Mon Sep 17 00:00:00 2001 From: DoubleMathew Date: Wed, 22 Apr 2026 15:15:42 -0500 Subject: [PATCH 2/8] fix/llamacpp_prebuilt_install (#5135) * fix/llamacpp_prebuilt_installinclude libllama-common.so in bundle as its needed by llama.cpp now * include support for rocm linux --- studio/install_llama_prebuilt.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index fd2b78d05f..1ad1ccc851 100755 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -3640,6 +3640,7 @@ def runtime_patterns_for_choice(choice: AssetChoice) -> list[str]: return [ "llama-server", "llama-quantize", + "libllama-common.so*", "libllama.so*", "libggml.so*", "libggml-base.so*", @@ -4929,6 +4930,7 @@ def load_prebuilt_metadata(install_dir: Path) -> dict[str, Any] | None: def runtime_payload_health_groups(choice: AssetChoice) -> list[list[str]]: if choice.install_kind == "linux-cpu": return [ + ["libllama-common.so*"], ["libllama.so*"], ["libggml.so*"], ["libggml-base.so*"], @@ -4937,6 +4939,7 @@ def runtime_payload_health_groups(choice: AssetChoice) -> list[list[str]]: ] if choice.install_kind == "linux-cuda": return [ + ["libllama-common.so*"], ["libllama.so*"], ["libggml.so*"], ["libggml-base.so*"], @@ -4952,6 +4955,7 @@ def runtime_payload_health_groups(choice: AssetChoice) -> list[list[str]]: ] if choice.install_kind == "linux-rocm": return [ + ["libllama-common.so*"], ["libllama.so*"], ["libggml.so*"], ["libggml-base.so*"], From 41a6cc8692bd47ba0e4590b45bdf88d24b15ef33 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 23 Apr 2026 00:22:40 -0700 Subject: [PATCH 3/8] Studio: split vision-cache exception test to match transient vs permanent (#5145) `TestVisionCacheOnException::test_exception_result_cached` currently patches `load_model_config` with `side_effect=OSError("network down")` and asserts `assert_called_once()`. That assertion is impossible by design: `_is_vision_model_uncached` in `studio/backend/utils/models/model_config.py` intentionally returns `None` for `OSError` so `is_vision_model` does not cache the fallback and retries on the next call. The module docstring on `_vision_detection_cache` itself spells this out: Only definitive results (True/False from successful detection) are cached; transient failures (network errors, timeouts) are NOT cached so they can be retried. The test has been failing identically on every downstream review run against `unslothai/unsloth` main (e.g. `unsloth#5115`, `unsloth#5080`), but the failure is not introduced by any of those PRs and does not gate correctness. Fix the collision by splitting the class into the two contracts the code actually implements: 1. `test_permanent_exception_result_cached` keeps the original intent ("exception falls back to False and that False is cached") but uses `ValueError`, which is one of the exception types `_is_vision_model_uncached` treats as permanent and caches. No `huggingface_hub` import needed. 2. `test_transient_exception_not_cached` pins the opposite contract with the original `OSError("network down")`: the call returns False but the second invocation re-runs detection (`call_count == 2`). This guards against a future regression where somebody caches transient failures and then users with a flaky network permanently see wrong detection for a model. Both tests use `assert ... is False` on the public API and mock-count assertions on `load_model_config`; no private helpers are touched. --- studio/backend/tests/test_vision_cache.py | 45 ++++++++++++++++++----- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/studio/backend/tests/test_vision_cache.py b/studio/backend/tests/test_vision_cache.py index fae1e95311..9e7bbdd1fb 100644 --- a/studio/backend/tests/test_vision_cache.py +++ b/studio/backend/tests/test_vision_cache.py @@ -124,23 +124,50 @@ class TestVisionCacheSubprocessPath: class TestVisionCacheOnException: - """When detection raises an exception, _is_vision_model_uncached catches - it and returns False. That False must be cached so subsequent calls don't - retry and fail again.""" + """When detection raises an exception, _is_vision_model_uncached + distinguishes permanent failures (cached as False) from transient + failures (returned as None, not cached so the next call can retry). + Verify both contracts.""" + + @patch( + "utils.models.model_config.load_model_config", + side_effect = ValueError("bad config"), + ) + @patch("utils.transformers_version.needs_transformers_5", return_value = False) + def test_permanent_exception_result_cached(self, mock_needs_t5, mock_load_config): + """A permanent failure (ValueError / RepositoryNotFoundError / + GatedRepoError / JSONDecodeError) should be caught, return False, + and that False should be cached so subsequent calls don't retry. + + ValueError is used here because it's the simplest of the + code-path's cacheable exception types and does not require an + import of huggingface_hub errors (whose module path varies + across versions).""" + # First call: load_model_config raises -> except branch -> False. + assert is_vision_model("broken/model") is False + # Second call: cache hit, load_model_config not called again. + assert is_vision_model("broken/model") is False + mock_load_config.assert_called_once() @patch( "utils.models.model_config.load_model_config", side_effect = OSError("network down"), ) @patch("utils.transformers_version.needs_transformers_5", return_value = False) - def test_exception_result_cached(self, mock_needs_t5, mock_load_config): - """A real exception inside _is_vision_model_uncached should be caught, - return False, and that False should be cached for subsequent calls.""" - # First call: load_model_config raises → except branch → False + def test_transient_exception_not_cached(self, mock_needs_t5, mock_load_config): + """A transient failure (OSError, timeouts) should return None from + _is_vision_model_uncached, surface as False to the caller, and + NOT be cached, so the next call retries detection. This matches + the documented behaviour on _vision_detection_cache: + 'transient failures (network errors, timeouts) are NOT cached so + they can be retried.'""" + # First call: load_model_config raises OSError -> uncached None + # -> caller returns False without caching. assert is_vision_model("broken/model") is False - # Second call: cache hit, load_model_config not called again + # Second call: cache miss again, load_model_config called a + # second time. assert is_vision_model("broken/model") is False - mock_load_config.assert_called_once() + assert mock_load_config.call_count == 2 # --------------------------------------------------------------------------- From 72c1c3b254cdd6034c01f4d0ec0e4866ad7e6ee6 Mon Sep 17 00:00:00 2001 From: Octopus Date: Thu, 23 Apr 2026 18:11:35 +0800 Subject: [PATCH 4/8] fix: patch CONTROL type for special tokens in sentencepiece GGUF export (#5080) * fix: patch CONTROL type for special tokens in sentencepiece GGUF export (fixes #5070) When converting a Gemma 3 fine-tune to GGUF via save_pretrained_gguf, tokens like (id=105) and (id=106) are already present in the sentencepiece model but are typed as NORMAL (1) instead of CONTROL (3). llama.cpp only recognises CONTROL tokens when parse_special=True is active, so these tokens get BPE-split during chat inference and the model produces garbage output. fix_sentencepiece_gguf now reads tokenizer.json's added_tokens list and, for any token with "special": true whose ID falls within the existing sentencepiece vocabulary, updates its type from NORMAL to CONTROL before writing the patched tokenizer.model to disk. The same CONTROL type is also applied when new tokens are appended for the out-of-range case, so both code paths are consistent. * Wire fix_sentencepiece_gguf into tokenizer save path and guard np.diff - save.py: call fix_sentencepiece_gguf inside unsloth_tokenizer_save_pretrained after _preserve_sentencepiece_tokenizer_assets. The helper was previously unreferenced in the repo, so the PR's CONTROL-type patch never actually ran during save_pretrained_gguf. - tokenizer_utils.py: add an early-return guard for len(added_tokens_ids) < 2 before the existing np.diff contiguity check. np.diff on a single-element array returns [] and .min() raises ValueError, which would discard the new in-vocab CONTROL patch; the guard flushes tokenizer.model first. Guard is inserted before the existing lines (diff = np.diff(...) and the min/max check) so their blame is unchanged. Dropped the separate refactor to fold the four duplicated "if patched > 0: write tokenizer.model" blocks into a helper because doing so re-indents lines whose blame is "Formatting & bug fixes"; the duplication remains the author's pattern. * Fix review findings: negative token_id guard and np.diff single-element - tokenizer_utils.py:481: add 0 <= lower bound to the special_token_ids bounds check. Previously a negative token_id from tokenizer.json passed 'token_id < sentence_piece_size' and Python's negative indexing wrapped tokenizer_file.pieces[-1] to silently corrupt the last piece to CONTROL. - tokenizer_utils.py:513: replace the loop-1 'if len < 2: return' guard (which was too broad: it silently skipped vocab extension for single-entry added_tokens.json) with a pre-pass that substitutes a trivially-contiguous 2-element sentinel for the contiguity check, then restores the original array before the append loop. Lines 519 ('diff = np.diff(added_tokens_ids)') and 520-529 (min/max/boundary checks and early-return write blocks) are left literally unchanged so blame remains intact. * Restore real added_tokens_ids before min boundary check Move the '_real_added_tokens_ids' restore above the 'added_tokens_ids.min() != sentence_piece_size' check. With the previous order the sentinel [sentence_piece_size, sentence_piece_size + 1] was still in scope when the min check ran, so any single-entry added_tokens .json with an out-of-range start id (e.g. 99 when sentence_piece_size=2) bypassed the boundary check and fell through to the append loop. * Scope fix_sentencepiece_gguf to GGUF export path only Previously wired fix_sentencepiece_gguf into unsloth_tokenizer_save_pretrained, which is the generic monkey-patch replacement for every tokenizer.save_pretrained call. That caused the GGUF-specific mutation (and the unconditional protobuf import in fix_sentencepiece_gguf) to run on every LoRA / merged 16-bit / push_to_hub / torchao save, where it has no purpose and can abort the entire save if the protobuf runtime is unavailable. - save.py: remove fix_sentencepiece_gguf call from unsloth_tokenizer_save_pretrained. - save.py: add the call inside unsloth_save_pretrained_gguf immediately before save_to_gguf, wrapped in try/except so a protobuf import failure logs a warning and lets GGUF conversion proceed rather than aborting the save. * Broaden special-token retag to USER_DEFINED and narrow save.py except - tokenizer_utils.py:483: the in-vocab retag previously only promoted NORMAL pieces to CONTROL, but the real Gemma tokenizer (e.g. unsloth/functiongemma -270m-it) stores / as USER_DEFINED (type 4). Extend the predicate to cover both NORMAL and USER_DEFINED so tokens marked "special": true in tokenizer.json are promoted regardless of their current sentencepiece type. Only tokens explicitly flagged special are touched, so non-special USER_DEFINED pieces are unchanged; already-CONTROL pieces stay unchanged. The warning message is generalised accordingly. - save.py:2294: narrow the except clause from Exception to ImportError. The loop-3 try/except was added to tolerate a missing protobuf runtime; leaving it broad also swallows OSError/PermissionError mid-write, which would ship a corrupted tokenizer.model to save_to_gguf. ImportError still covers the protobuf case while letting I/O errors propagate to the outer save handler. * Harden fix_sentencepiece_gguf: widen except, protobuf fallback, revert USER_DEFINED widen, guard entry id - save.py:2294: widen except from ImportError back to Exception. The loop-4 narrowing let JSONDecodeError / KeyError / OSError / PermissionError from fix_sentencepiece_gguf abort the entire GGUF export, a regression vs pre-PR behavior. The outer save_to_gguf try/except still covers GGUF-side failures; any fix-side failure now logs a typed warning and lets conversion proceed. - tokenizer_utils.py:445: the direct 'from transformers.utils import sentencepiece_model_pb2' raises TypeError ("Descriptors cannot be created directly") on modern protobuf runtimes. Prepend a sys.modules.setdefault pre-population using transformers.convert_slow_tokenizer.import_protobuf() so the subsequent from-import finds a compatible module via the module cache. The original import line is left verbatim at its place as the final resolver. - tokenizer_utils.py:483: revert loop-4 widening; retag only NORMAL pieces to CONTROL. Retagging USER_DEFINED pieces caused a concrete tokenization regression where an intentionally-USER_DEFINED in-vocab special token had its sentencepiece encoding broken (' hello' changed from [11, 3, 8] to [11, 0, 12, 21, 0, 8]). The PR's stated scope is the NORMAL->CONTROL Gemma case; USER_DEFINED handling is deferred. - tokenizer_utils.py:475: defensive guard around entry["id"]. A malformed added_tokens entry missing the "id" field or with a non-int id is now skipped rather than raising KeyError / inserting garbage. * Add review tests for sentencepiece GGUF fix * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: octo-patch Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Co-authored-by: Daniel Han Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .../test_fix_sentencepiece_gguf_robustness.py | 132 ++++++++++++++++++ unsloth/save.py | 9 ++ unsloth/tokenizer_utils.py | 72 +++++++++- 3 files changed, 210 insertions(+), 3 deletions(-) create mode 100644 tests/saving/test_fix_sentencepiece_gguf_robustness.py diff --git a/tests/saving/test_fix_sentencepiece_gguf_robustness.py b/tests/saving/test_fix_sentencepiece_gguf_robustness.py new file mode 100644 index 0000000000..49bd70fa2f --- /dev/null +++ b/tests/saving/test_fix_sentencepiece_gguf_robustness.py @@ -0,0 +1,132 @@ +import ast +import json +import os + +os.environ.setdefault("PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION", "python") + +from transformers.utils import sentencepiece_model_pb2 + +from unsloth.tokenizer_utils import fix_sentencepiece_gguf + + +NORMAL, CONTROL, USER_DEFINED = 1, 3, 4 + +_SAVE_PY = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "unsloth", "save.py") +) +_TOK_PY = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "..", "unsloth", "tokenizer_utils.py") +) + + +def _build(pieces): + m = sentencepiece_model_pb2.ModelProto() + for piece, score, typ in pieces: + p = m.pieces.add() + p.piece = piece + p.score = score + p.type = typ + return m.SerializeToString() + + +def _read(path): + m = sentencepiece_model_pb2.ModelProto() + with open(path, "rb") as f: + m.ParseFromString(f.read()) + return [(p.piece, p.type) for p in m.pieces] + + +def test_user_defined_special_piece_is_not_retyped(tmp_path): + pieces = [ + ("", 0.0, CONTROL), + ("a", -1.0, NORMAL), + ("", -1.0, USER_DEFINED), + ] + (tmp_path / "tokenizer.model").write_bytes(_build(pieces)) + (tmp_path / "tokenizer.json").write_text( + json.dumps( + {"added_tokens": [{"id": 2, "content": "", "special": True}]} + ) + ) + fix_sentencepiece_gguf(str(tmp_path)) + got = dict(_read(str(tmp_path / "tokenizer.model"))) + assert got[""] == USER_DEFINED + + +def test_malformed_entry_missing_id_does_not_raise(tmp_path): + pieces = [("", 0.0, CONTROL), ("a", -1.0, NORMAL), ("", -1.0, NORMAL)] + (tmp_path / "tokenizer.model").write_bytes(_build(pieces)) + (tmp_path / "tokenizer.json").write_text( + json.dumps( + { + "added_tokens": [ + {"content": "no_id_entry", "special": True}, + {"id": 2, "content": "", "special": True}, + ] + } + ) + ) + fix_sentencepiece_gguf(str(tmp_path)) + got = dict(_read(str(tmp_path / "tokenizer.model"))) + assert got[""] == CONTROL + + +def test_entry_with_non_int_id_is_skipped(tmp_path): + pieces = [("", 0.0, CONTROL), ("a", -1.0, NORMAL)] + (tmp_path / "tokenizer.model").write_bytes(_build(pieces)) + (tmp_path / "tokenizer.json").write_text( + json.dumps({"added_tokens": [{"id": "oops", "content": "x", "special": True}]}) + ) + before = (tmp_path / "tokenizer.model").read_bytes() + fix_sentencepiece_gguf(str(tmp_path)) + after = (tmp_path / "tokenizer.model").read_bytes() + assert before == after + + +def test_save_py_except_clause_is_broad_exception(): + with open(_SAVE_PY) as f: + tree = ast.parse(f.read()) + for node in ast.walk(tree): + if ( + isinstance(node, ast.FunctionDef) + and node.name == "unsloth_save_pretrained_gguf" + ): + for subnode in ast.walk(node): + if isinstance(subnode, ast.Try): + body_src = "\n".join(ast.unparse(s) for s in subnode.body) + if "fix_sentencepiece_gguf(" not in body_src: + continue + handler = subnode.handlers[0] + assert handler.type is not None + assert isinstance(handler.type, ast.Name) + assert handler.type.id == "Exception" + return + raise AssertionError( + "fix_sentencepiece_gguf try block not found in unsloth_save_pretrained_gguf" + ) + + +def test_tokenizer_utils_uses_import_protobuf_fallback_pattern(): + with open(_TOK_PY) as f: + src = f.read() + tree = ast.parse(src) + for node in ast.walk(tree): + if isinstance(node, ast.FunctionDef) and node.name == "fix_sentencepiece_gguf": + fn_src = ast.unparse(node) + assert "import_protobuf" in fn_src + return + raise AssertionError("fix_sentencepiece_gguf not found in tokenizer_utils.py") + + +def test_all_special_tokens_are_gated_by_tokenizer_json_not_by_type(tmp_path): + pieces = [ + ("", 0.0, CONTROL), + ("a", -1.0, NORMAL), + ("", -1.0, NORMAL), + ("", -1.0, USER_DEFINED), + ] + (tmp_path / "tokenizer.model").write_bytes(_build(pieces)) + fix_sentencepiece_gguf(str(tmp_path)) + got = dict(_read(str(tmp_path / "tokenizer.model"))) + assert got[""] == NORMAL + assert got[""] == USER_DEFINED diff --git a/unsloth/save.py b/unsloth/save.py index 4e9e1bfe11..df3df13a04 100644 --- a/unsloth/save.py +++ b/unsloth/save.py @@ -2288,6 +2288,15 @@ def unsloth_save_pretrained_gguf( quant_method = "q8_0" quantization_methods.append(quant_method.lower()) + try: + from .tokenizer_utils import fix_sentencepiece_gguf + + fix_sentencepiece_gguf(save_directory) + except Exception as e: + logger.warning( + f"Unsloth: fix_sentencepiece_gguf skipped ({type(e).__name__}): {e}" + ) + try: all_file_locations, want_full_precision, is_vlm_update = save_to_gguf( model_name = model_name, diff --git a/unsloth/tokenizer_utils.py b/unsloth/tokenizer_utils.py index 85b3c5308b..130894e385 100644 --- a/unsloth/tokenizer_utils.py +++ b/unsloth/tokenizer_utils.py @@ -434,8 +434,25 @@ def fix_sentencepiece_gguf(saved_location): Fixes sentencepiece tokenizers which did not extend the vocabulary with user defined tokens. Inspiration from https://github.com/ggerganov/llama.cpp/blob/master/convert_hf_to_gguf.py + + Also fixes special tokens (e.g. Gemma 3's /) that are + already present in the sentencepiece model but are incorrectly typed as NORMAL instead + of CONTROL. This causes them to be written to GGUF with token_type=1 (NORMAL) instead + of token_type=3 (CONTROL), which breaks chat inference in llama.cpp since parse_special + only matches CONTROL tokens. """ from copy import deepcopy + import sys + + try: + from transformers.convert_slow_tokenizer import import_protobuf + + sys.modules.setdefault( + "transformers.utils.sentencepiece_model_pb2", + import_protobuf(), + ) + except Exception: + pass from transformers.utils import sentencepiece_model_pb2 import json from enum import IntEnum @@ -457,12 +474,45 @@ def fix_sentencepiece_gguf(saved_location): ) sentence_piece_size = len(tokenizer_file.pieces) + # Build a set of token IDs that are marked as special in tokenizer.json. + # These tokens should use CONTROL type in the sentencepiece model so that + # llama.cpp writes them as CONTROL (type=3) in the GGUF token_type array. + special_token_ids = set() + if os.path.isfile(f"{saved_location}/tokenizer.json"): + with open(f"{saved_location}/tokenizer.json", "r", encoding = "utf-8") as f: + tokenizer_json = json.load(f) + for entry in tokenizer_json.get("added_tokens", []): + token_id = entry.get("id") + if entry.get("special", False) and isinstance(token_id, int): + special_token_ids.add(token_id) + + # Fix existing sentencepiece tokens that are marked as special in tokenizer.json + # but have the wrong type (NORMAL instead of CONTROL) in the sentencepiece model. + patched = 0 + for token_id in special_token_ids: + if 0 <= token_id < sentence_piece_size: + piece = tokenizer_file.pieces[token_id] + if piece.type == SentencePieceTokenTypes.NORMAL: + piece.type = SentencePieceTokenTypes.CONTROL + patched += 1 + if patched > 0: + logger.warning( + f"Unsloth: Patched {patched} special token(s) in {saved_location}/tokenizer.model " + f"from NORMAL to CONTROL type so llama.cpp / GGUF chat inference works correctly." + ) + # Load added_tokens_json if not os.path.isfile(f"{saved_location}/added_tokens.json"): + if patched > 0: + with open(f"{saved_location}/tokenizer.model", "wb") as file: + file.write(tokenizer_file.SerializeToString()) return with open(f"{saved_location}/added_tokens.json", "r", encoding = "utf-8") as file: added_tokens_json = json.load(file) if len(added_tokens_json) == 0: + if patched > 0: + with open(f"{saved_location}/tokenizer.model", "wb") as file: + file.write(tokenizer_file.SerializeToString()) return added_tokens_json = dict( @@ -472,10 +522,20 @@ def fix_sentencepiece_gguf(saved_location): # Confirm added_tokens_json is correct added_tokens_ids = np.array(list(added_tokens_json.values())) + _real_added_tokens_ids = added_tokens_ids + if len(added_tokens_ids) < 2: + added_tokens_ids = np.array([sentence_piece_size, sentence_piece_size + 1]) diff = np.diff(added_tokens_ids) if diff.min() != 1 or diff.max() != 1: + if patched > 0: + with open(f"{saved_location}/tokenizer.model", "wb") as file: + file.write(tokenizer_file.SerializeToString()) return + added_tokens_ids = _real_added_tokens_ids if added_tokens_ids.min() != sentence_piece_size: + if patched > 0: + with open(f"{saved_location}/tokenizer.model", "wb") as file: + file.write(tokenizer_file.SerializeToString()) return # Edit sentence piece tokens with added_tokens_json @@ -485,10 +545,16 @@ def fix_sentencepiece_gguf(saved_location): f"But we need to extend to sentencepiece vocab size ({new_size})." ) new_tokens = deepcopy(tokenizer_file.pieces[-len(added_tokens_ids) :]) - for new_token, added_token in zip(new_tokens, added_tokens_json.keys()): - new_token.piece = added_token.encode("utf-8") + for new_token, added_token_str in zip(new_tokens, added_tokens_json.keys()): + added_token_id = added_tokens_json[added_token_str] + new_token.piece = added_token_str.encode("utf-8") new_token.score = -1000.0 - new_token.type = SentencePieceTokenTypes.USER_DEFINED + # Use CONTROL type for tokens marked as special in tokenizer.json, + # otherwise fall back to USER_DEFINED. + if added_token_id in special_token_ids: + new_token.type = SentencePieceTokenTypes.CONTROL + else: + new_token.type = SentencePieceTokenTypes.USER_DEFINED tokenizer_file.pieces.extend(new_tokens) From 114908cd9fbc6921dd1dd3839b5bfcc6c707b909 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 23 Apr 2026 04:46:03 -0700 Subject: [PATCH 5/8] fix(install): clear STUDIO_LOCAL_* env on POSIX normal install (#5146) install.sh's normal-install branch passed the inherited parent-shell environment to setup.sh without resetting STUDIO_LOCAL_INSTALL or STUDIO_LOCAL_REPO. Consumers treat either as truthy: studio/setup.sh:491 checks STUDIO_LOCAL_INSTALL != "1" studio/install_python_stack.py:868 reads STUDIO_LOCAL_REPO, falsy if empty Net effect: if a user or developer shell has stale STUDIO_LOCAL_* exports from a previous --local run, a subsequent 'normal' install or desktop-managed install silently takes the local-dev path: version checks are skipped and an editable overlay points at the stale repo. Fix mirrors install.ps1:1082-1087 on Windows: set STUDIO_LOCAL_INSTALL=0 and STUDIO_LOCAL_REPO= explicitly in the env prefix for the non-local branch so setup.sh sees a clean state regardless of parent exports. Co-authored-by: Daniel Han --- install.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/install.sh b/install.sh index 0352887de1..b1663ba0ff 100755 --- a/install.sh +++ b/install.sh @@ -1587,8 +1587,15 @@ if [ "$STUDIO_LOCAL_INSTALL" = true ]; then UNSLOTH_NO_TORCH="$SKIP_TORCH" \ bash "$SETUP_SH" Date: Thu, 23 Apr 2026 13:50:10 +0200 Subject: [PATCH 6/8] Add tauri (#5144) * add unsloth studio desktop app * Fix review findings - studio/src-tauri/tauri.conf.json: retarget updater to staging repo (danielhanchen/unsloth-staging-2); switch to unslothai/unsloth on upstream merge. - studio/src-tauri/linux/postremove.sh: drop the interactive read loop and the /home/* iteration. Package maintainer scripts must stay non-interactive and must not touch other users' data. - studio/frontend/src/app/auth-guards.ts: honor tauriAutoAuth() boolean. Failed auto-auth now redirects to /login; requireGuest/requirePasswordChangeFlow only redirect to /chat when auth succeeds. The new early-return on failed auth is intentional so the login / change-password flows remain reachable when desktop auth is not yet established. - studio/frontend/src/config/env.ts: keep fetched=false on health failure so later calls retry instead of caching the client-side platform guess. - studio/src-tauri/src/install.rs: pick the available system package manager (apt-get, dnf, zypper, pacman); AppImage bundles run on non-Debian distros. - studio/frontend/src/lib/open-link.ts + markdown-text/sources callers: return boolean from openLink so callers only preventDefault on handled URLs; relative hrefs now navigate natively. - studio/frontend/src/features/settings/tabs/about-tab.tsx: fetch(apiUrl(...)) so the version request targets the backend port in desktop mode. The bare /api/health predates the Tauri webview (blame: the earlier onboarding commit, which ran with same-origin frontend/backend); in desktop mode the webview origin is tauri://localhost so the bare path fails. - install.ps1: gate the install_python_stack.py hotfix on a sentinel comment instead of a content regex; append the sentinel after applying so reruns are unambiguous. - unsloth_cli/commands/studio.py _write_auth_secret: use the atomic mkstemp + os.replace path on Windows too; chmod calls are wrapped in try/except OSError. - studio/src-tauri/src/preflight.rs probe_existing_backends: fan out the health probes concurrently; desktop-auth status still runs sequentially per candidate. reqwest::Client is internally Arc-wrapped so the in-loop .clone() is a refcount bump, not a deep clone; annotated inline. - studio/src-tauri/src/preflight.rs run_cli_probe: wait() after kill() to reap the child, matching probe_cli_capability. - studio/src-tauri/src/process.rs + main.rs: add stop_backend_detached and use it from the tray quit handler so the 5s graceful-wait does not block the Tauri main loop. RunEvent::Exit keeps the synchronous safety-net call. - studio/backend/main.py: drop the permissive localhost CORS regex in api-only mode; the explicit allow_origins list is sufficient. - .github/workflows/release-desktop.yml: drop max-parallel: 1 so platform builds run in parallel, and lift releaseBody to an env var so the three tauri-action invocations share one source of truth. * Fix review findings (loop 2) - studio/backend/auth/storage.py update_password: clear_desktop_secret() alongside clear_bootstrap_password() so rotating the admin password also revokes any previously provisioned .desktop_secret. Without this, an old local desktop credential keeps minting fresh admin tokens via /api/auth/desktop-login after a password rotation. - studio/src-tauri/src/desktop_auth.rs provision_desktop_auth: wrap cmd.output().await in tokio::time::timeout(30s). DESKTOP_AUTH_LOCK is held across the whole desktop_auth flow, and previously a hanging `unsloth studio provision-desktop-auth` subprocess would pin the lock indefinitely and freeze every subsequent desktop_auth call. * Add review tests * Consolidate review tests Merge review-added tests into the existing studio/backend/tests/test_desktop_auth.py (the PR's authoritative desktop-auth test file). Drops three scaffolding files under tests/python/ in favor of five focused tests next to the tests they extend: - test_update_password_clears_desktop_secret (runtime) - test_update_password_on_unknown_user_leaves_desktop_secret_intact (runtime) - test_cli_provisioning_delegates_to_storage_create_desktop_secret (source-level) - test_cli_connect_auth_db_reads_storage_db_path (source-level) - test_desktop_auth_provision_has_bounded_timeout (Rust source-level) * Revert auth-guards.ts Tauri branches to unconditional form The review loop on PR 5144 introduced a regression: the isTauri branch of requireAuth redirected to /login when tauriAutoAuth() returned false, and requireGuest / requirePasswordChangeFlow silently fell through on the same condition. The Tauri desktop app authenticates via a local auto-generated secret; it must never surface /login or /change-password to the user. A failed auto-auth should let the startup layer retry, not expose a password form. Restore the three Tauri branches to the author's original unconditional form (requireAuth: return; requireGuest / requirePasswordChangeFlow: throw redirect({to: '/chat'})). Keep the rest of the review fixes -- the apiUrl() fetch wrapping, authRedirect helper, and fetchAuthStatus refactor are all legitimate improvements and are preserved. * Revert release-desktop.yml to author's version The review loop's workflow-file tweaks (drop max-parallel: 1, lift releaseBody to an env var) are cosmetic. OAuth tokens cannot push workflow-file changes, and fine-grained PATs cannot honor maintainerCanModify on a third-party fork. Reverting the workflow file to wasimysaid's version lets the push go through without needing a classic PAT with both repo and workflow scopes. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Co-authored-by: Daniel Han Co-authored-by: Daniel Han Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .github/workflows/release-desktop.yml | 187 + .gitignore | 12 + install.ps1 | 78 +- install.sh | 69 +- studio/backend/auth/authentication.py | 50 +- studio/backend/auth/storage.py | 121 +- .../data_recipe/local_callable_validators.py | 6 + studio/backend/core/inference/audio_codecs.py | 9 +- studio/backend/core/inference/llama_cpp.py | 8 + studio/backend/core/training/trainer.py | 9 +- studio/backend/loggers/config.py | 8 + studio/backend/main.py | 17 +- studio/backend/models/auth.py | 6 + studio/backend/routes/auth.py | 25 +- studio/backend/routes/data_recipe/jobs.py | 15 + studio/backend/run.py | 23 +- studio/backend/tests/test_desktop_auth.py | 597 ++ studio/backend/utils/hardware/nvidia.py | 8 + studio/backend/utils/models/model_config.py | 5 + studio/backend/utils/subprocess_compat.py | 34 + studio/backend/utils/transformers_version.py | 6 + studio/frontend/package.json | 4 + studio/frontend/public/studio.png | Bin 0 -> 27420 bytes studio/frontend/src/app/auth-guards.ts | 65 +- studio/frontend/src/app/provider.tsx | 167 +- studio/frontend/src/app/routes/__root.tsx | 7 +- .../components/assistant-ui/markdown-text.tsx | 9 +- .../src/components/assistant-ui/sources.tsx | 15 +- .../src/components/tauri/startup-screen.tsx | 401 ++ .../src/components/tauri/update-banner.tsx | 84 + .../src/components/tauri/update-screen.tsx | 173 + .../src/components/ui/shimmer-button.tsx | 96 + studio/frontend/src/config/env.ts | 31 +- studio/frontend/src/features/auth/api.ts | 49 +- .../features/auth/components/auth-form.tsx | 16 +- studio/frontend/src/features/auth/index.ts | 5 + studio/frontend/src/features/auth/session.ts | 2 + .../src/features/auth/tauri-auto-auth.ts | 95 + .../src/features/settings/tabs/about-tab.tsx | 3 +- studio/frontend/src/hooks/index.ts | 1 + studio/frontend/src/hooks/use-gpu-info.ts | 3 +- .../frontend/src/hooks/use-tauri-backend.ts | 487 ++ studio/frontend/src/hooks/use-tauri-update.ts | 219 + studio/frontend/src/lib/api-base.ts | 24 + studio/frontend/src/lib/open-link.ts | 31 + studio/install_llama_prebuilt.py | 25 + studio/install_python_stack.py | 46 +- studio/setup.ps1 | 10 +- studio/setup.sh | 15 +- studio/src-tauri/Cargo.lock | 6320 +++++++++++++++++ studio/src-tauri/Cargo.toml | 42 + studio/src-tauri/Entitlements.plist | 15 + studio/src-tauri/build.rs | 3 + studio/src-tauri/capabilities/default.json | 24 + studio/src-tauri/icons/128x128.png | Bin 0 -> 9194 bytes studio/src-tauri/icons/32x32.png | Bin 0 -> 1930 bytes studio/src-tauri/icons/icon.icns | Bin 0 -> 263568 bytes studio/src-tauri/icons/icon.ico | Bin 0 -> 34589 bytes studio/src-tauri/icons/icon.png | Bin 0 -> 46007 bytes studio/src-tauri/linux/postremove.sh | 9 + studio/src-tauri/src/commands.rs | 476 ++ studio/src-tauri/src/desktop_auth.rs | 388 + studio/src-tauri/src/install.rs | 555 ++ studio/src-tauri/src/main.rs | 189 + studio/src-tauri/src/preflight.rs | 831 +++ studio/src-tauri/src/process.rs | 537 ++ studio/src-tauri/src/update.rs | 293 + studio/src-tauri/src/windows_job.rs | 72 + studio/src-tauri/tauri.conf.json | 66 + studio/src-tauri/tauri.macos.conf.json | 11 + studio/src-tauri/tauri.windows.conf.json | 16 + .../windows/branding/nsis-header.bmp | Bin 0 -> 25818 bytes .../windows/branding/nsis-sidebar.bmp | Bin 0 -> 154542 bytes studio/src-tauri/windows/hooks.nsh | 8 + tests/sh/test_tauri_install_exit_order.sh | 95 + unsloth_cli/commands/studio.py | 341 +- 76 files changed, 13533 insertions(+), 134 deletions(-) create mode 100644 .github/workflows/release-desktop.yml create mode 100644 studio/backend/tests/test_desktop_auth.py create mode 100644 studio/backend/utils/subprocess_compat.py create mode 100644 studio/frontend/public/studio.png create mode 100644 studio/frontend/src/components/tauri/startup-screen.tsx create mode 100644 studio/frontend/src/components/tauri/update-banner.tsx create mode 100644 studio/frontend/src/components/tauri/update-screen.tsx create mode 100644 studio/frontend/src/components/ui/shimmer-button.tsx create mode 100644 studio/frontend/src/features/auth/tauri-auto-auth.ts create mode 100644 studio/frontend/src/hooks/use-tauri-backend.ts create mode 100644 studio/frontend/src/hooks/use-tauri-update.ts create mode 100644 studio/frontend/src/lib/api-base.ts create mode 100644 studio/frontend/src/lib/open-link.ts create mode 100644 studio/src-tauri/Cargo.lock create mode 100644 studio/src-tauri/Cargo.toml create mode 100644 studio/src-tauri/Entitlements.plist create mode 100644 studio/src-tauri/build.rs create mode 100644 studio/src-tauri/capabilities/default.json create mode 100644 studio/src-tauri/icons/128x128.png create mode 100644 studio/src-tauri/icons/32x32.png create mode 100644 studio/src-tauri/icons/icon.icns create mode 100644 studio/src-tauri/icons/icon.ico create mode 100644 studio/src-tauri/icons/icon.png create mode 100755 studio/src-tauri/linux/postremove.sh create mode 100644 studio/src-tauri/src/commands.rs create mode 100644 studio/src-tauri/src/desktop_auth.rs create mode 100644 studio/src-tauri/src/install.rs create mode 100644 studio/src-tauri/src/main.rs create mode 100644 studio/src-tauri/src/preflight.rs create mode 100644 studio/src-tauri/src/process.rs create mode 100644 studio/src-tauri/src/update.rs create mode 100644 studio/src-tauri/src/windows_job.rs create mode 100644 studio/src-tauri/tauri.conf.json create mode 100644 studio/src-tauri/tauri.macos.conf.json create mode 100644 studio/src-tauri/tauri.windows.conf.json create mode 100644 studio/src-tauri/windows/branding/nsis-header.bmp create mode 100644 studio/src-tauri/windows/branding/nsis-sidebar.bmp create mode 100644 studio/src-tauri/windows/hooks.nsh create mode 100644 tests/sh/test_tauri_install_exit_order.sh diff --git a/.github/workflows/release-desktop.yml b/.github/workflows/release-desktop.yml new file mode 100644 index 0000000000..2d1c6f51f2 --- /dev/null +++ b/.github/workflows/release-desktop.yml @@ -0,0 +1,187 @@ +name: Release Desktop App + +on: + workflow_dispatch: + inputs: + draft: + description: 'Create as draft release' + type: boolean + default: true + +permissions: + contents: write + +jobs: + build: + strategy: + fail-fast: false + max-parallel: 1 + matrix: + include: + - platform: macos-latest + args: '--target aarch64-apple-darwin' + label: macOS (Apple Silicon) + # - platform: macos-latest + # args: '--target x86_64-apple-darwin' + # label: macOS (Intel) + - platform: ubuntu-22.04 + args: '' + label: Linux (x64) + - platform: windows-latest + args: '' + label: Windows (x64) + + name: Build ${{ matrix.label }} + runs-on: ${{ matrix.platform }} + + env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + + + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 + + # ── Linux dependencies ── + - name: Install Linux dependencies + if: matrix.platform == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev libxdo-dev libssl-dev patchelf + + # ── Node.js ── + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 24 + + - name: Install frontend dependencies + working-directory: studio/frontend + run: npm install + + # ── Rust ── + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }} + + - name: Rust cache + uses: swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae + with: + workspaces: 'studio/src-tauri -> target' + + # ── macOS: import signing certificate ── + - name: Import Apple certificate + if: matrix.platform == 'macos-latest' + env: + APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} + APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12 + security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security default-keychain -s build.keychain + security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain + security set-keychain-settings -t 3600 -u build.keychain + security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain + security find-identity -v -p codesigning build.keychain + rm -f certificate.p12 + + # ── Windows: install Azure Trusted Signing CLI ── + - name: Install trusted-signing-cli + if: matrix.platform == 'windows-latest' + run: | + cargo install trusted-signing-cli --version 0.9.0 --locked + echo "$env:USERPROFILE\.cargo\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + # ── Windows: verify signing CLI is accessible ── + - name: Verify trusted-signing-cli + if: matrix.platform == 'windows-latest' + run: | + Write-Output "PATH: $env:PATH" + Get-Command trusted-signing-cli -ErrorAction SilentlyContinue || Write-Output "trusted-signing-cli NOT in PATH" + trusted-signing-cli --version || Write-Output "trusted-signing-cli failed to run" + + # ── Linux: build + sign + upload ── + - name: Build Linux app + if: matrix.platform == 'ubuntu-22.04' + uses: tauri-apps/tauri-action@fce9c6108b31ea247710505d3aaaa893ee6768d4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + with: + projectPath: studio + tagName: desktop-v__VERSION__ + releaseName: 'Unsloth Studio (Desktop) v__VERSION__' + releaseBody: | + Desktop app for Unsloth Studio. + + **macOS**: Download the Apple Silicon `.dmg`. + **Windows**: Download the `-setup.exe` installer. + **Linux**: Download `.deb` (Ubuntu/Debian) or `.AppImage` (universal). + + > Linux in-app updates are AppImage-oriented. Package installs should update by downloading a new package. + > Linux AppImage on Ubuntu 24.04+ may require: `sudo apt install libfuse2t64` + releaseDraft: ${{ inputs.draft }} + prerelease: false + args: -v ${{ matrix.args }} + + # ── macOS: build + sign + notarize + upload ── + - name: Build macOS app + if: matrix.platform == 'macos-latest' + uses: tauri-apps/tauri-action@fce9c6108b31ea247710505d3aaaa893ee6768d4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + with: + projectPath: studio + tagName: desktop-v__VERSION__ + releaseName: 'Unsloth Studio (Desktop) v__VERSION__' + releaseBody: | + Desktop app for Unsloth Studio. + + **macOS**: Download the Apple Silicon `.dmg`. + **Windows**: Download the `-setup.exe` installer. + **Linux**: Download `.deb` (Ubuntu/Debian) or `.AppImage` (universal). + + > Linux in-app updates are AppImage-oriented. Package installs should update by downloading a new package. + > Linux AppImage on Ubuntu 24.04+ may require: `sudo apt install libfuse2t64` + releaseDraft: ${{ inputs.draft }} + prerelease: false + args: -v ${{ matrix.args }} + + # ── Windows: build + sign + upload ── + - name: Build Windows app + if: matrix.platform == 'windows-latest' + uses: tauri-apps/tauri-action@fce9c6108b31ea247710505d3aaaa893ee6768d4 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} + AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} + AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }} + AZURE_CERTIFICATE_PROFILE_NAME: ${{ secrets.AZURE_CERTIFICATE_PROFILE_NAME }} + with: + projectPath: studio + tagName: desktop-v__VERSION__ + releaseName: 'Unsloth Studio (Desktop) v__VERSION__' + releaseBody: | + Desktop app for Unsloth Studio. + + **macOS**: Download the Apple Silicon `.dmg`. + **Windows**: Download the `-setup.exe` installer. + **Linux**: Download `.deb` (Ubuntu/Debian) or `.AppImage` (universal). + + > Linux in-app updates are AppImage-oriented. Package installs should update by downloading a new package. + > Linux AppImage on Ubuntu 24.04+ may require: `sudo apt install libfuse2t64` + releaseDraft: ${{ inputs.draft }} + prerelease: false + args: -v ${{ matrix.args }} diff --git a/.gitignore b/.gitignore index 7a24d07c6f..b6786ee655 100644 --- a/.gitignore +++ b/.gitignore @@ -204,6 +204,18 @@ tmp/ **/node_modules/ auth.db +# Tauri local build/generated output +studio/src-tauri/target/ +studio/src-tauri/gen/ +studio/src-tauri/artifacts/ +studio/src-tauri/icons/android/ +studio/src-tauri/icons/ios/ +studio/src-tauri/icons/128x128@2x.png +studio/src-tauri/icons/64x64.png +studio/src-tauri/icons/Square*Logo.png +studio/src-tauri/icons/StoreLogo.png +studio/src-tauri/icons/squarehq.png + # Local working docs **/CLAUDE.md **/claude.md diff --git a/install.ps1 b/install.ps1 index 7f98fc5cb8..44464101f3 100644 --- a/install.ps1 +++ b/install.ps1 @@ -12,11 +12,13 @@ function Install-UnslothStudio { $StudioLocalInstall = $false $PackageName = "unsloth" $RepoRoot = "" + $TauriMode = $false $SkipTorch = $false $argList = $args for ($i = 0; $i -lt $argList.Count; $i++) { switch ($argList[$i]) { "--local" { $StudioLocalInstall = $true } + "--tauri" { $TauriMode = $true } "--no-torch" { $SkipTorch = $true } "--verbose" { $script:UnslothVerbose = $true } "-v" { $script:UnslothVerbose = $true } @@ -44,6 +46,20 @@ function Install-UnslothStudio { } } + # Validate --package to prevent injection into shell/Python commands + if ($PackageName -notmatch '^[a-zA-Z0-9][a-zA-Z0-9._-]*$') { + Write-Host "[ERROR] --package name contains invalid characters (allowed: a-z A-Z 0-9 . _ -)" -ForegroundColor Red + return + } + + # ── Tauri structured output ── + function Write-TauriLog { + param([string]$Tag, [string]$Message) + if ($TauriMode) { + Write-Host "[TAURI:$Tag] $Message" + } + } + $PythonVersion = "3.13" $StudioHome = Join-Path $env:USERPROFILE ".unsloth\studio" $VenvDir = Join-Path $StudioHome "unsloth_studio" @@ -609,6 +625,7 @@ shell.Run cmd, 0, False } # ── Check winget ── + Write-TauriLog "STEP" "Checking system dependencies" if (-not (Get-Command winget -ErrorAction SilentlyContinue)) { step "winget" "not available" "Red" substep "Install it from https://aka.ms/getwinget" "Yellow" @@ -688,6 +705,7 @@ shell.Run cmd, 0, False # ── Install Python if no compatible version (3.11-3.13) found ── # Find-CompatiblePython returns @{ Version = "3.13"; Path = "C:\...\python.exe" } or $null. + Write-TauriLog "STEP" "Installing Python" $DetectedPython = Find-CompatiblePython if ($DetectedPython) { step "python" "Python $($DetectedPython.Version) already installed" @@ -736,6 +754,7 @@ shell.Run cmd, 0, False } # ── Install uv if not present ── + Write-TauriLog "STEP" "Installing uv package manager" if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { substep "installing uv package manager..." $prevEAP = $ErrorActionPreference @@ -746,7 +765,7 @@ shell.Run cmd, 0, False # Fallback: if winget didn't put uv on PATH, try the PowerShell installer if (-not (Get-Command uv -ErrorAction SilentlyContinue)) { substep "trying alternative uv installer..." "Yellow" - powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" + Invoke-Expression (Invoke-RestMethod -Uri "https://astral.sh/uv/install.ps1") Refresh-SessionPath } } @@ -760,6 +779,7 @@ shell.Run cmd, 0, False # ── 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. + Write-TauriLog "STEP" "Creating virtual environment" if (-not (Test-Path $StudioHome)) { New-Item -ItemType Directory -Path $StudioHome -Force | Out-Null } @@ -806,6 +826,7 @@ shell.Run cmd, 0, False substep "$VenvDir" $venvExit = Invoke-InstallCommand { uv venv $VenvDir --python "$($DetectedPython.Path)" } if ($venvExit -ne 0) { + Write-TauriLog "ERROR" "Failed to create virtual environment (exit code $venvExit)" Write-Host "[ERROR] Failed to create virtual environment (exit code $venvExit)" -ForegroundColor Red return } @@ -908,6 +929,7 @@ shell.Run cmd, 0, False if ($_Migrated) { # Migrated env: force-reinstall unsloth+unsloth-zoo to ensure clean state # in the new venv location, while preserving existing torch/CUDA + Write-TauriLog "STEP" "Installing unsloth" substep "upgrading unsloth in migrated environment..." if ($SkipTorch) { # No-torch: install unsloth + unsloth-zoo with --no-deps, then @@ -938,14 +960,17 @@ shell.Run cmd, 0, False if ($SkipTorch) { substep "skipping PyTorch (--no-torch flag set)." "Yellow" } else { + Write-TauriLog "STEP" "Installing PyTorch" substep "installing PyTorch ($TorchIndexUrl)..." $torchInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython "torch>=2.4,<2.11.0" torchvision torchaudio --index-url $TorchIndexUrl } if ($torchInstallExit -ne 0) { + Write-TauriLog "ERROR" "Failed to install PyTorch (exit code $torchInstallExit)" Write-Host "[ERROR] Failed to install PyTorch (exit code $torchInstallExit)" -ForegroundColor Red return } } + Write-TauriLog "STEP" "Installing unsloth" substep "installing unsloth (this may take a few minutes)..." if ($SkipTorch) { # No-torch: install unsloth + unsloth-zoo with --no-deps, then @@ -960,9 +985,10 @@ shell.Run cmd, 0, False } elseif ($StudioLocalInstall) { $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "unsloth>=2026.4.7" unsloth-zoo } } else { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth "$PackageName" } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --upgrade-package unsloth -- "$PackageName" } } if ($baseInstallExit -ne 0) { + Write-TauriLog "ERROR" "Failed to install unsloth (exit code $baseInstallExit)" Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red return } @@ -977,6 +1003,7 @@ shell.Run cmd, 0, False } } else { # Fallback: GPU detection failed to produce a URL -- let uv resolve torch + Write-TauriLog "STEP" "Installing unsloth" substep "installing unsloth (this may take a few minutes)..." if ($StudioLocalInstall) { $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython unsloth-zoo "unsloth>=2026.4.7" --torch-backend=auto } @@ -991,20 +1018,52 @@ shell.Run cmd, 0, False return } } else { - $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython "$PackageName" --torch-backend=auto } + $baseInstallExit = Invoke-InstallCommand { uv pip install --python $VenvPython --torch-backend=auto -- "$PackageName" } if ($baseInstallExit -ne 0) { + Write-TauriLog "ERROR" "Failed to install unsloth (exit code $baseInstallExit)" Write-Host "[ERROR] Failed to install unsloth (exit code $baseInstallExit)" -ForegroundColor Red return } } } + # Hotfix: patch install_python_stack.py for Windows GUI stdout + # The PyPI version crashes with OSError when stdout is piped from a GUI app. + # Copy our fixed version (bundled by Tauri) over the installed one. + # Remove this block once PyPI ships the fix from commit 18c5aae7. + if ($TauriMode) { + $rawPath = if ($PSCommandPath) { $PSCommandPath } else { $MyInvocation.ScriptName } + $scriptDir = Split-Path -Parent ($rawPath -replace '^\\\\\?\\', '') + $fixedPy = Join-Path $scriptDir "install_python_stack.py" + $target = Join-Path $VenvDir "Lib\site-packages\studio\install_python_stack.py" + $sentinel = "# UNSLOTH_DESKTOP_HOTFIX_APPLIED_v1" + $sentinelPattern = [regex]::Escape($sentinel) + if ((Test-Path $fixedPy) -and (Test-Path $target)) { + $installed = Get-Content $target -Raw + if ($installed -notmatch $sentinelPattern) { + Copy-Item $fixedPy $target -Force + Add-Content -Path $target -Value "`n$sentinel" + substep "patched install_python_stack.py (stdout fix)" + } else { + substep "install_python_stack.py already has stdout fix" + } + } elseif ((Test-Path $fixedPy) -and (Test-Path (Split-Path $target))) { + Copy-Item $fixedPy $target -Force + Add-Content -Path $target -Value "`n$sentinel" + substep "patched install_python_stack.py (stdout fix)" + } else { + Write-Host "[WARN] Could not patch install_python_stack.py (bundled file or target dir missing)" -ForegroundColor Yellow + } + } + # ── Run studio setup ── # setup.ps1 will handle installing Git, CMake, Visual Studio Build Tools, # CUDA Toolkit, Node.js, and other dependencies automatically via winget. + Write-TauriLog "STEP" "Running studio setup" step "setup" "running unsloth studio setup..." $UnslothExe = Join-Path $VenvDir "Scripts\unsloth.exe" if (-not (Test-Path $UnslothExe)) { + Write-TauriLog "ERROR" "unsloth CLI was not installed correctly" Write-Host "[ERROR] unsloth CLI was not installed correctly." -ForegroundColor Red Write-Host " Expected: $UnslothExe" -ForegroundColor Yellow Write-Host " This usually means an older unsloth version was installed that does not include the Studio CLI." -ForegroundColor Yellow @@ -1015,6 +1074,8 @@ shell.Run cmd, 0, False $env:SKIP_STUDIO_BASE = "1" $env:STUDIO_PACKAGE_NAME = $PackageName $env:UNSLOTH_NO_TORCH = if ($SkipTorch) { "true" } else { "false" } + # Tauri desktop app bundles its own frontend — skip Node/npm/frontend build + $env:SKIP_STUDIO_FRONTEND = if ($TauriMode) { "1" } else { "0" } # Always set STUDIO_LOCAL_INSTALL explicitly to avoid stale values from # a previous --local run in the same PowerShell session. if ($StudioLocalInstall) { @@ -1032,12 +1093,11 @@ shell.Run cmd, 0, False & $UnslothExe @studioArgs $setupExit = $LASTEXITCODE if ($setupExit -ne 0) { + Write-TauriLog "ERROR" "unsloth studio setup failed (exit code $setupExit)" Write-Host "[ERROR] unsloth studio setup failed (exit code $setupExit)" -ForegroundColor Red return } - New-StudioShortcuts -UnslothExePath $UnslothExe - # ── Expose `unsloth` via a shim dir containing only unsloth.exe ── # We do NOT add the venv Scripts dir to PATH (it also holds python.exe # and pip.exe, which would hijack the user's system interpreter). @@ -1109,6 +1169,14 @@ shell.Run cmd, 0, False } Refresh-SessionPath # sync current session with registry + # ── Tauri mode: done, skip shortcuts and auto-launch ── + if ($TauriMode) { + Write-TauriLog "DONE" "" + return + } + + New-StudioShortcuts -UnslothExePath $UnslothExe + # Launch studio automatically in interactive terminals; # in non-interactive environments (CI, Docker) just print instructions. $IsInteractive = [Environment]::UserInteractive -and (-not [Console]::IsInputRedirected) diff --git a/install.sh b/install.sh index b1663ba0ff..07473e441d 100755 --- a/install.sh +++ b/install.sh @@ -35,6 +35,7 @@ substep() { printf " ${C_DIM}%-15s${2:-$C_DIM}%s${C_RST}\n" "" "$1"; } # ── Parse flags ── STUDIO_LOCAL_INSTALL=false PACKAGE_NAME="unsloth" +TAURI_MODE=false _USER_PYTHON="" _NO_TORCH_FLAG=false _VERBOSE=false @@ -54,6 +55,7 @@ for arg in "$@"; do case "$arg" in --local) STUDIO_LOCAL_INSTALL=true ;; --package) _next_is_package=true ;; + --tauri) TAURI_MODE=true ;; --python) _next_is_python=true ;; --no-torch) _NO_TORCH_FLAG=true ;; --verbose|-v) _VERBOSE=true ;; @@ -142,6 +144,24 @@ if [ "$_next_is_python" = true ]; then exit 1 fi +# Validate --package to prevent injection into shell/Python commands. +# Must start with a letter/digit (rejects leading dashes that uv would parse as flags). +case "$PACKAGE_NAME" in + [!a-zA-Z0-9]*) + echo "❌ ERROR: --package name must start with a letter or digit." >&2 + exit 1 ;; + *[!a-zA-Z0-9._-]*) + echo "❌ ERROR: --package name contains invalid characters (allowed: a-z A-Z 0-9 . _ -)" >&2 + exit 1 ;; +esac + +# ── Tauri structured output ── +tauri_log() { + if [ "$TAURI_MODE" = true ]; then + echo "[TAURI:$1] $2" + fi +} + PYTHON_VERSION="" # resolved after platform detection STUDIO_HOME="$HOME/.unsloth/studio" VENV_DIR="$STUDIO_HOME/unsloth_studio" @@ -192,6 +212,12 @@ _smart_apt_install() { return 0 fi + # In Tauri mode, report needed packages and exit — Rust handles elevation + if [ "$TAURI_MODE" = true ]; then + tauri_log "NEED_SUDO" "$_STILL_MISSING" + exit 2 + fi + # Step 3: Escalate -- need elevated permissions for remaining packages if command -v sudo >/dev/null 2>&1; then echo "" @@ -752,6 +778,7 @@ printf " ${C_DIM}%s${C_RST}\n" "$RULE" echo "" # ── Detect platform ── +tauri_log "STEP" "Detecting platform" OS="linux" if [ "$(uname)" = "Darwin" ]; then OS="macos" @@ -804,6 +831,7 @@ fi # ── Check system dependencies ── # cmake and git are needed by unsloth studio setup to build the GGUF inference # engine (llama.cpp). build-essential and libcurl-dev are also needed on Linux. +tauri_log "STEP" "Checking system dependencies" MISSING="" command -v cmake >/dev/null 2>&1 || MISSING="$MISSING cmake" @@ -868,6 +896,7 @@ else fi # ── Install uv ── +tauri_log "STEP" "Installing uv package manager" UV_MIN_VERSION="0.7.14" version_ge() { @@ -922,6 +951,7 @@ if ! command -v uv >/dev/null 2>&1 || ! _uv_version_ok uv; then fi # ── Create venv (migrate old layout if possible, otherwise fresh) ── +tauri_log "STEP" "Creating virtual environment" mkdir -p "$STUDIO_HOME" _MIGRATED=false @@ -1304,6 +1334,7 @@ case "$TORCH_INDEX_URL" in esac # ── Install unsloth directly into the venv (no activation needed) ── +tauri_log "STEP" "Installing PyTorch" _VENV_PY="$VENV_DIR/bin/python" if [ "$_MIGRATED" = true ]; then # Migrated env: force-reinstall unsloth+unsloth-zoo to ensure clean state @@ -1481,6 +1512,7 @@ elif [ -n "$TORCH_INDEX_URL" ]; then esac fi # Fresh: Step 2 - install unsloth, preserving pre-installed torch + tauri_log "STEP" "Installing Unsloth" substep "installing unsloth (this may take a few minutes)..." if [ "$SKIP_TORCH" = true ]; then # No-torch: install unsloth + unsloth-zoo with --no-deps, then @@ -1503,7 +1535,7 @@ elif [ -n "$TORCH_INDEX_URL" ]; then run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps else run_install_cmd "install unsloth" uv pip install --python "$_VENV_PY" \ - --upgrade-package unsloth "$PACKAGE_NAME" + --upgrade-package unsloth -- "$PACKAGE_NAME" fi # AMD ROCm: repair torch if the unsloth/unsloth-zoo install pulled in # CUDA torch from PyPI, overwriting the ROCm wheels installed in Step 1. @@ -1523,17 +1555,19 @@ elif [ -n "$TORCH_INDEX_URL" ]; then fi else # Fallback: GPU detection failed to produce a URL -- let uv resolve torch + tauri_log "STEP" "Installing Unsloth" substep "installing unsloth (this may take a few minutes)..." if [ "$STUDIO_LOCAL_INSTALL" = true ]; then run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" unsloth-zoo "unsloth>=2026.4.7" --torch-backend=auto substep "overlaying local repo (editable)..." run_install_cmd "overlay local repo" uv pip install --python "$_VENV_PY" -e "$_REPO_ROOT" --no-deps else - run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" "$PACKAGE_NAME" --torch-backend=auto + run_install_cmd "install unsloth (auto torch backend)" uv pip install --python "$_VENV_PY" --torch-backend=auto -- "$PACKAGE_NAME" fi fi # ── Run studio setup ── +tauri_log "STEP" "Running Studio setup" # When --local, use the repo's own setup.sh directly. # Otherwise, find it inside the installed package. SETUP_SH="" @@ -1554,6 +1588,7 @@ if [ -z "$SETUP_SH" ] || [ ! -f "$SETUP_SH" ]; then fi if [ -z "$SETUP_SH" ] || [ ! -f "$SETUP_SH" ]; then + tauri_log "ERROR" "Could not find studio/setup.sh in the installed package" echo "❌ ERROR: Could not find studio/setup.sh in the installed package." exit 1 fi @@ -1571,16 +1606,16 @@ if ! command -v bash >/dev/null 2>&1; then fi step "setup" "running unsloth studio update..." -# install.sh already installs base packages (unsloth + unsloth-zoo) and -# no-torch-runtime.txt above, so tell install_python_stack.py to skip -# the base step to avoid redundant reinstallation. _SKIP_BASE=1 -# Run setup.sh outside set -e so that a llama.cpp build failure (exit 1) -# does not skip PATH setup, shortcuts, and launch below. We capture the -# exit code and propagate it after post-install steps finish. _SETUP_EXIT=0 +# Tauri desktop app bundles its own frontend — skip Node/npm/frontend build +_SKIP_FRONTEND=0 +if [ "$TAURI_MODE" = true ]; then + _SKIP_FRONTEND=1 +fi if [ "$STUDIO_LOCAL_INSTALL" = true ]; then SKIP_STUDIO_BASE="$_SKIP_BASE" \ + SKIP_STUDIO_FRONTEND="$_SKIP_FRONTEND" \ STUDIO_PACKAGE_NAME="$PACKAGE_NAME" \ STUDIO_LOCAL_INSTALL=1 \ STUDIO_LOCAL_REPO="$_REPO_ROOT" \ @@ -1593,6 +1628,7 @@ else # local-dev path in setup.sh and install_python_stack.py. Mirrors the # reset already done in install.ps1 for PowerShell. SKIP_STUDIO_BASE="$_SKIP_BASE" \ + SKIP_STUDIO_FRONTEND="$_SKIP_FRONTEND" \ STUDIO_PACKAGE_NAME="$PACKAGE_NAME" \ STUDIO_LOCAL_INSTALL=0 \ STUDIO_LOCAL_REPO= \ @@ -1629,23 +1665,26 @@ case ":$PATH:" in ;; esac -create_studio_shortcuts "$VENV_ABS_BIN/unsloth" "$OS" +# Non-Tauri installs keep shortcuts even if setup reports failure. +if [ "$TAURI_MODE" != true ]; then + create_studio_shortcuts "$VENV_ABS_BIN/unsloth" "$OS" +fi # If setup.sh failed, report and exit now. # PATH and shortcuts are already set up so the user can fix and retry. if [ "$_SETUP_EXIT" -ne 0 ]; then echo "" step "error" "studio setup failed (exit code $_SETUP_EXIT)" "$C_ERR" - substep "Check the output above for details, then re-run:" - if [ "$STUDIO_LOCAL_INSTALL" = true ]; then - substep " unsloth studio update --local" - else - substep " unsloth studio update" - fi echo "" exit "$_SETUP_EXIT" fi +# ── Tauri mode: done, skip shortcuts and auto-launch ── +if [ "$TAURI_MODE" = true ]; then + tauri_log "DONE" "" + exit 0 +fi + echo "" printf " ${C_TITLE}%s${C_RST}\n" "Unsloth Studio installed!" printf " ${C_DIM}%s${C_RST}\n" "$RULE" diff --git a/studio/backend/auth/authentication.py b/studio/backend/auth/authentication.py index da59ba9a1a..6ddcbc8e0b 100644 --- a/studio/backend/auth/authentication.py +++ b/studio/backend/auth/authentication.py @@ -52,6 +52,8 @@ def _decode_subject_without_verification(token: str) -> Optional[str]: def create_access_token( subject: str, expires_delta: Optional[timedelta] = None, + *, + desktop: bool = False, ) -> str: """ Create a signed JWT for the given subject (e.g. username). @@ -59,6 +61,8 @@ def create_access_token( Tokens are valid across restarts because the signing secret is stored in SQLite. """ to_encode = {"sub": subject} + if desktop: + to_encode["desktop"] = True expire = datetime.now(timezone.utc) + ( expires_delta or timedelta(minutes = ACCESS_TOKEN_EXPIRE_MINUTES) ) @@ -70,7 +74,29 @@ def create_access_token( ) -def create_refresh_token(subject: str) -> str: +def is_desktop_access_token(token: str) -> bool: + """Return true only for a valid desktop-issued JWT access token.""" + if token.startswith(API_KEY_PREFIX): + return False + + subject = _decode_subject_without_verification(token) + if subject is None: + return False + + record = get_user_and_secret(subject) + if record is None: + return False + + _salt, _pwd_hash, jwt_secret, _must_change_password = record + try: + payload = jwt.decode(token, jwt_secret, algorithms = [ALGORITHM]) + except jwt.InvalidTokenError: + return False + + return payload.get("sub") == subject and payload.get("desktop") is True + + +def create_refresh_token(subject: str, *, desktop: bool = False) -> str: """ Create a random refresh token, store its hash in SQLite, and return it. @@ -78,21 +104,28 @@ def create_refresh_token(subject: str) -> str: """ token = secrets.token_urlsafe(48) expires_at = datetime.now(timezone.utc) + timedelta(days = REFRESH_TOKEN_EXPIRE_DAYS) - save_refresh_token(token, subject, expires_at.isoformat()) + save_refresh_token(token, subject, expires_at.isoformat(), is_desktop = desktop) return token -def refresh_access_token(refresh_token: str) -> Tuple[Optional[str], Optional[str]]: +def refresh_access_token( + refresh_token: str, +) -> Tuple[Optional[str], Optional[str], bool]: """ Validate a refresh token and issue a new access token. The refresh token itself is NOT consumed — it stays valid until expiry. Returns a new access_token or None if the refresh token is invalid/expired. """ - username = verify_refresh_token(refresh_token) - if username is None: - return None, None - return create_access_token(subject = username), username + verified = verify_refresh_token(refresh_token) + if verified is None: + return None, None, False + username, is_desktop = verified + return ( + create_access_token(subject = username, desktop = is_desktop), + username, + is_desktop, + ) def reload_secret() -> None: @@ -173,7 +206,8 @@ async def _get_current_subject( status_code = status.HTTP_401_UNAUTHORIZED, detail = "Invalid token payload", ) - if must_change_password and not allow_password_change: + is_desktop = payload.get("desktop") is True + if must_change_password and not allow_password_change and not is_desktop: raise HTTPException( status_code = status.HTTP_403_FORBIDDEN, detail = "Password change required", diff --git a/studio/backend/auth/storage.py b/studio/backend/auth/storage.py index 7d55a2dc59..2b0e359d39 100644 --- a/studio/backend/auth/storage.py +++ b/studio/backend/auth/storage.py @@ -6,6 +6,7 @@ SQLite storage for authentication data (user credentials + JWT secret). """ import hashlib +import os import secrets import sqlite3 from datetime import datetime, timezone @@ -54,6 +55,10 @@ def generate_bootstrap_password() -> str: # before the user changes the password. ensure_dir(_BOOTSTRAP_PW_PATH.parent) _BOOTSTRAP_PW_PATH.write_text(_bootstrap_password) + try: + os.chmod(_BOOTSTRAP_PW_PATH, 0o600) + except OSError: + pass return _bootstrap_password @@ -63,6 +68,17 @@ def get_bootstrap_password() -> Optional[str]: return _bootstrap_password +def _load_bootstrap_password() -> Optional[str]: + """Load an existing bootstrap password without creating one.""" + global _bootstrap_password + _bootstrap_password = None + if _BOOTSTRAP_PW_PATH.is_file(): + bootstrap_password = _BOOTSTRAP_PW_PATH.read_text().strip() + if bootstrap_password: + _bootstrap_password = bootstrap_password + return _bootstrap_password + + def clear_bootstrap_password() -> None: """Delete the persisted bootstrap password file (called after password change).""" global _bootstrap_password @@ -114,7 +130,8 @@ def get_connection() -> sqlite3.Connection: id INTEGER PRIMARY KEY, token_hash TEXT NOT NULL, username TEXT NOT NULL, - expires_at TEXT NOT NULL + expires_at TEXT NOT NULL, + is_desktop INTEGER NOT NULL DEFAULT 0 ); """ ) @@ -146,6 +163,13 @@ def get_connection() -> sqlite3.Connection: conn.execute( "ALTER TABLE auth_user ADD COLUMN must_change_password INTEGER NOT NULL DEFAULT 0" ) + refresh_columns = { + row["name"] for row in conn.execute("PRAGMA table_info(refresh_tokens)") + } + if "is_desktop" not in refresh_columns: + conn.execute( + "ALTER TABLE refresh_tokens ADD COLUMN is_desktop INTEGER NOT NULL DEFAULT 0" + ) conn.commit() return conn @@ -201,6 +225,9 @@ def _get_or_create_api_key_pbkdf2_salt() -> bytes: _API_KEY_PBKDF2_ITERATIONS = 100_000 +DESKTOP_SECRET_PREFIX = "desktop-" +_DESKTOP_SECRET_HASH_KEY = "desktop_secret_hash" +_DESKTOP_SECRET_CREATED_AT_KEY = "desktop_secret_created_at" def _pbkdf2_api_key(raw_key: str) -> str: @@ -233,6 +260,10 @@ def _pbkdf2_api_key(raw_key: str) -> str: return dk.hex() +def _pbkdf2_desktop_secret(raw_secret: str) -> str: + return _pbkdf2_api_key(raw_secret) + + def is_initialized() -> bool: """Check if auth is ready for login (at least one user exists in DB).""" conn = get_connection() @@ -374,6 +405,10 @@ def ensure_default_admin() -> bool: Uses a randomly generated diceware passphrase as the bootstrap password. Returns True when the default admin was created in this call. """ + if get_user_and_secret(DEFAULT_ADMIN_USERNAME) is not None: + _load_bootstrap_password() + return False + bootstrap_pw = generate_bootstrap_password() try: create_initial_user( @@ -406,12 +441,19 @@ def update_password(username: str, new_password: str) -> bool: conn.commit() if cursor.rowcount > 0: clear_bootstrap_password() + clear_desktop_secret() return cursor.rowcount > 0 finally: conn.close() -def save_refresh_token(token: str, username: str, expires_at: str) -> None: +def save_refresh_token( + token: str, + username: str, + expires_at: str, + *, + is_desktop: bool = False, +) -> None: """ Store a hashed refresh token with its associated username and expiry. """ @@ -420,21 +462,21 @@ def save_refresh_token(token: str, username: str, expires_at: str) -> None: try: conn.execute( """ - INSERT INTO refresh_tokens (token_hash, username, expires_at) - VALUES (?, ?, ?) + INSERT INTO refresh_tokens (token_hash, username, expires_at, is_desktop) + VALUES (?, ?, ?, ?) """, - (token_hash, username, expires_at), + (token_hash, username, expires_at, int(is_desktop)), ) conn.commit() finally: conn.close() -def verify_refresh_token(token: str) -> Optional[str]: +def verify_refresh_token(token: str) -> Optional[Tuple[str, bool]]: """ - Verify a refresh token and return the username. + Verify a refresh token and return the username plus desktop marker. - Returns the username if valid and not expired, None otherwise. + Returns the username and desktop marker if valid and not expired, None otherwise. The token is NOT consumed — it stays valid until it expires. """ token_hash = _hash_token(token) @@ -449,7 +491,7 @@ def verify_refresh_token(token: str) -> Optional[str]: cur = conn.execute( """ - SELECT id, username, expires_at FROM refresh_tokens + SELECT id, username, expires_at, is_desktop FROM refresh_tokens WHERE token_hash = ? """, (token_hash,), @@ -465,7 +507,7 @@ def verify_refresh_token(token: str) -> Optional[str]: conn.commit() return None - return row["username"] + return row["username"], bool(row["is_desktop"]) finally: conn.close() @@ -480,6 +522,65 @@ def revoke_user_refresh_tokens(username: str) -> None: conn.close() +def create_desktop_secret() -> str: + """Create/rotate the local desktop credential and return it once.""" + ensure_default_admin() + raw_secret = DESKTOP_SECRET_PREFIX + secrets.token_urlsafe(48) + secret_hash = _pbkdf2_desktop_secret(raw_secret) + now = datetime.now(timezone.utc).isoformat() + conn = get_connection() + try: + conn.execute( + "INSERT OR REPLACE INTO app_secrets (key, value) VALUES (?, ?)", + (_DESKTOP_SECRET_HASH_KEY, secret_hash), + ) + conn.execute( + "INSERT OR REPLACE INTO app_secrets (key, value) VALUES (?, ?)", + (_DESKTOP_SECRET_CREATED_AT_KEY, now), + ) + conn.commit() + return raw_secret + finally: + conn.close() + + +def validate_desktop_secret(raw_secret: str) -> Optional[str]: + """Return the real admin username when the desktop secret matches.""" + if not raw_secret.startswith(DESKTOP_SECRET_PREFIX): + return None + if get_user_and_secret(DEFAULT_ADMIN_USERNAME) is None: + return None + + secret_hash = _pbkdf2_desktop_secret(raw_secret) + conn = get_connection() + try: + cur = conn.execute( + "SELECT value FROM app_secrets WHERE key = ?", + (_DESKTOP_SECRET_HASH_KEY,), + ) + row = cur.fetchone() + if row is None: + return None + if not secrets.compare_digest(row["value"], secret_hash): + return None + return DEFAULT_ADMIN_USERNAME + finally: + conn.close() + + +def clear_desktop_secret() -> None: + """Remove backend-side desktop auth state.""" + conn = get_connection() + try: + conn.execute( + "DELETE FROM app_secrets WHERE key IN (?, ?)", + (_DESKTOP_SECRET_HASH_KEY, _DESKTOP_SECRET_CREATED_AT_KEY), + ) + conn.commit() + finally: + conn.close() + + # --------------------------------------------------------------------------- # API key management # --------------------------------------------------------------------------- diff --git a/studio/backend/core/data_recipe/local_callable_validators.py b/studio/backend/core/data_recipe/local_callable_validators.py index c32b2fccaf..afd10b02d1 100644 --- a/studio/backend/core/data_recipe/local_callable_validators.py +++ b/studio/backend/core/data_recipe/local_callable_validators.py @@ -33,6 +33,11 @@ _OXC_TOOL_DIR = Path(__file__).resolve().parent / "oxc-validator" _OXC_RUNNER_PATH = _OXC_TOOL_DIR / "validate.mjs" +from utils.subprocess_compat import ( + windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, +) + + @dataclass(frozen = True) class OxcLocalCallableValidatorSpec: name: str @@ -256,6 +261,7 @@ def _run_oxc_batch( capture_output = True, check = False, env = env, + **_windows_hidden_subprocess_kwargs(), ) except (OSError, ValueError) as exc: logger.warning("OXC subprocess launch failed: %s", exc) diff --git a/studio/backend/core/inference/audio_codecs.py b/studio/backend/core/inference/audio_codecs.py index bcf3ec2937..895b112e85 100644 --- a/studio/backend/core/inference/audio_codecs.py +++ b/studio/backend/core/inference/audio_codecs.py @@ -8,6 +8,7 @@ Supports: SNAC (Orpheus), CSM (Sesame), BiCodec (Spark), DAC (OuteTTS) import io import re +import subprocess import wave import structlog from loggers import get_logger @@ -16,6 +17,10 @@ from typing import Optional, Tuple import numpy as np import torch +from utils.subprocess_compat import ( + windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, +) + logger = get_logger(__name__) @@ -81,7 +86,6 @@ class AudioCodecManager: return import os import sys - import subprocess # Clone SparkAudio/Spark-TTS GitHub repo for the sparktts Python package # (same approach as training — the HF model repos don't contain the package) @@ -101,6 +105,7 @@ class AudioCodecManager: spark_code_dir, ], check = True, + **_windows_hidden_subprocess_kwargs(), ) if spark_code_dir not in sys.path: @@ -119,7 +124,6 @@ class AudioCodecManager: return import os import sys - import subprocess # Clone OuteTTS repo (same pattern as Spark-TTS / BiCodec) # The pip package has problematic dependencies; the notebook clones and @@ -139,6 +143,7 @@ class AudioCodecManager: outetts_code_dir, ], check = True, + **_windows_hidden_subprocess_kwargs(), ) # Remove files that pull in heavy / incompatible dependencies # (matches notebook: gguf_model.py is under models/, others under outetts/) diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index 2e26995309..6ad05ac52d 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -18,6 +18,7 @@ from loggers import get_logger import shutil import socket import subprocess +import sys import threading import time from pathlib import Path @@ -26,8 +27,13 @@ from urllib.parse import urlparse import httpx +from utils.subprocess_compat import ( + windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, +) + logger = get_logger(__name__) + # ── Pre-compiled patterns for plan-without-action re-prompt ── # Forward-looking intent signals that indicate the model is # describing what it *will* do rather than giving a final answer. @@ -440,6 +446,7 @@ class LlamaCppBackend: capture_output = True, text = True, timeout = 10, + **_windows_hidden_subprocess_kwargs(), ) if result.returncode != 0: return [] @@ -1658,6 +1665,7 @@ class LlamaCppBackend: stderr = subprocess.STDOUT, text = True, env = env, + **_windows_hidden_subprocess_kwargs(), ) # Start background thread to drain stdout and prevent pipe deadlock diff --git a/studio/backend/core/training/trainer.py b/studio/backend/core/training/trainer.py index 77cbda6b45..c1e2ac4a85 100644 --- a/studio/backend/core/training/trainer.py +++ b/studio/backend/core/training/trainer.py @@ -49,6 +49,7 @@ from unsloth.chat_templates import get_chat_template import json import threading import math +import subprocess import structlog from loggers import get_logger import time @@ -69,6 +70,10 @@ from utils.paths import ( ) from trl import SFTTrainer, SFTConfig +from utils.subprocess_compat import ( + windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, +) + logger = get_logger(__name__) @@ -1765,6 +1770,7 @@ class UnslothTrainer: spark_code_dir, ], check = True, + **_windows_hidden_subprocess_kwargs(), ) if spark_code_dir not in sys.path: @@ -1982,8 +1988,6 @@ class UnslothTrainer: device = "cuda" if torch.cuda.is_available() else "cpu" # Clone OuteTTS repo (same as audio_codecs._load_dac) - import subprocess - base_dir = os.path.dirname(os.path.abspath(__file__)) outetts_code_dir = os.path.join(base_dir, "inference", "OuteTTS") outetts_pkg = os.path.join(outetts_code_dir, "outetts") @@ -2000,6 +2004,7 @@ class UnslothTrainer: outetts_code_dir, ], check = True, + **_windows_hidden_subprocess_kwargs(), ) for fpath in [ os.path.join(outetts_pkg, "models", "gguf_model.py"), diff --git a/studio/backend/loggers/config.py b/studio/backend/loggers/config.py index 0d32a64657..4c0d8ade28 100644 --- a/studio/backend/loggers/config.py +++ b/studio/backend/loggers/config.py @@ -44,6 +44,14 @@ class LogConfig: # Fallback to INFO if an invalid level is provided log_level = getattr(logging, log_level_name, logging.INFO) + if sys.platform == "win32": + for stream in (sys.stdout, sys.stderr): + if hasattr(stream, "reconfigure"): + try: + stream.reconfigure(encoding = "utf-8", errors = "replace") + except Exception: + pass + structlog.configure( processors = [ # Reorder processors to control field order diff --git a/studio/backend/main.py b/studio/backend/main.py index d146a8ef12..05adcaa2ea 100644 --- a/studio/backend/main.py +++ b/studio/backend/main.py @@ -179,9 +179,22 @@ logger = LogConfig.setup_logging( app.add_middleware(LoggingMiddleware) # CORS middleware +_api_only = os.environ.get("UNSLOTH_API_ONLY") == "1" +_cors_origins = ["*"] +if _api_only: + _cors_origins = [ + "tauri://localhost", # Linux/macOS Tauri webview + "http://tauri.localhost", # Windows Tauri webview + "http://localhost", # dev fallback + ] + _cors_origin_regex = None +else: + _cors_origin_regex = None + app.add_middleware( CORSMiddleware, - allow_origins = ["*"], # In production, specify allowed origins + allow_origins = _cors_origins, + allow_origin_regex = _cors_origin_regex, allow_credentials = True, allow_methods = ["*"], allow_headers = ["*"], @@ -223,6 +236,8 @@ async def health_check(): "version": UNSLOTH_VERSION, "device_type": device_type, "chat_only": _hw_module.CHAT_ONLY, + "desktop_protocol_version": 1, + "supports_desktop_auth": True, } diff --git a/studio/backend/models/auth.py b/studio/backend/models/auth.py index c55e646508..23eb0ac4c0 100644 --- a/studio/backend/models/auth.py +++ b/studio/backend/models/auth.py @@ -17,6 +17,12 @@ class AuthLoginRequest(BaseModel): password: str = Field(..., description = "Password") +class DesktopLoginRequest(BaseModel): + """Desktop-only local secret exchange payload.""" + + secret: str = Field(..., description = "Desktop local auth secret") + + class RefreshTokenRequest(BaseModel): """Refresh token payload to obtain new access + refresh tokens.""" diff --git a/studio/backend/routes/auth.py b/studio/backend/routes/auth.py index 5cd23bd450..3deeb6793b 100644 --- a/studio/backend/routes/auth.py +++ b/studio/backend/routes/auth.py @@ -17,6 +17,7 @@ from models.auth import ( ChangePasswordRequest, CreateApiKeyRequest, CreateApiKeyResponse, + DesktopLoginRequest, RefreshTokenRequest, ) from models.users import Token @@ -80,6 +81,24 @@ async def login(payload: AuthLoginRequest) -> Token: ) +@router.post("/desktop-login", response_model = Token) +async def desktop_login(payload: DesktopLoginRequest) -> Token: + """Exchange a local desktop secret for normal admin-subject tokens.""" + username = storage.validate_desktop_secret(payload.secret) + if username is None: + raise HTTPException( + status_code = status.HTTP_401_UNAUTHORIZED, + detail = "Desktop authentication failed", + ) + + return Token( + access_token = create_access_token(subject = username, desktop = True), + refresh_token = create_refresh_token(subject = username, desktop = True), + token_type = "bearer", + must_change_password = False, + ) + + @router.post("/refresh", response_model = Token) async def refresh(payload: RefreshTokenRequest) -> Token: """ @@ -87,7 +106,7 @@ async def refresh(payload: RefreshTokenRequest) -> Token: The refresh token itself is reusable until it expires (7 days). """ - new_access_token, username = refresh_access_token(payload.refresh_token) + new_access_token, username, is_desktop = refresh_access_token(payload.refresh_token) if new_access_token is None or username is None: raise HTTPException( status_code = status.HTTP_401_UNAUTHORIZED, @@ -98,7 +117,9 @@ async def refresh(payload: RefreshTokenRequest) -> Token: access_token = new_access_token, refresh_token = payload.refresh_token, token_type = "bearer", - must_change_password = storage.requires_password_change(username), + must_change_password = False + if is_desktop + else storage.requires_password_change(username), ) diff --git a/studio/backend/routes/data_recipe/jobs.py b/studio/backend/routes/data_recipe/jobs.py index 00546b47a4..606ef1832c 100644 --- a/studio/backend/routes/data_recipe/jobs.py +++ b/studio/backend/routes/data_recipe/jobs.py @@ -57,6 +57,20 @@ def _resolve_local_v1_endpoint(request: Request) -> str: return f"http://127.0.0.1:{int(port)}/v1" +def _request_has_desktop_access_token(request: Request) -> bool: + auth_header = request.headers.get("authorization") + if not auth_header: + return False + + parts = auth_header.split(None, 1) + if len(parts) != 2 or parts[0].lower() != "bearer": + return False + + from auth.authentication import is_desktop_access_token + + return is_desktop_access_token(parts[1]) + + def _used_llm_model_aliases(recipe: dict[str, Any]) -> set[str]: """Return the set of model_aliases that are actually referenced by an LLM column. Used to narrow the "Chat model loaded" gate so that orphan @@ -154,6 +168,7 @@ def _inject_local_providers(recipe: dict[str, Any], request: Request) -> None: token = create_access_token( subject = "unsloth", expires_delta = timedelta(hours = 24), + desktop = _request_has_desktop_access_token(request), ) # Defensively strip any stale "external"-only fields the frontend may diff --git a/studio/backend/run.py b/studio/backend/run.py index 9675b9ea4c..7590ef1067 100644 --- a/studio/backend/run.py +++ b/studio/backend/run.py @@ -248,6 +248,7 @@ def run_server( port: int = 8888, frontend_path: Path = Path(__file__).resolve().parent.parent / "frontend" / "dist", silent: bool = False, + api_only: bool = False, llama_parallel_slots: int = 1, ): """ @@ -258,6 +259,7 @@ def run_server( port: Port to bind to (auto-increments if in use) frontend_path: Path to frontend build directory (optional) silent: Suppress startup messages + api_only: Run API server only, no frontend serving (for Tauri desktop app) llama_parallel_slots: Number of parallel slots for llama-server Note: @@ -275,6 +277,10 @@ def run_server( except Exception: pass + # Set env var BEFORE importing main so CORS middleware picks it up + if api_only: + os.environ["UNSLOTH_API_ONLY"] = "1" + import nest_asyncio nest_asyncio.apply() @@ -310,8 +316,12 @@ def run_server( print("=" * 50) print("") - # Setup frontend if path provided - if frontend_path: + # Output port for Tauri to parse when in api-only mode + if api_only: + print(f"TAURI_PORT={port}", flush = True) + + # Setup frontend if path provided (skip in api-only mode) + if frontend_path and not api_only: if setup_frontend(app, frontend_path): if not silent: print(f"[OK] Frontend loaded from {frontend_path}") @@ -391,10 +401,17 @@ if __name__ == "__main__": help = "Path to frontend build", ) parser.add_argument("--silent", action = "store_true", help = "Suppress output") + parser.add_argument( + "--api-only", + action = "store_true", + help = "API server only, no frontend (for Tauri)", + ) args = parser.parse_args() - kwargs = dict(host = args.host, port = args.port, silent = args.silent) + kwargs = dict( + host = args.host, port = args.port, silent = args.silent, api_only = args.api_only + ) if args.frontend is not None: kwargs["frontend_path"] = Path(args.frontend) diff --git a/studio/backend/tests/test_desktop_auth.py b/studio/backend/tests/test_desktop_auth.py new file mode 100644 index 0000000000..c8cf1c7081 --- /dev/null +++ b/studio/backend/tests/test_desktop_auth.py @@ -0,0 +1,597 @@ +import importlib.util +import asyncio +import hashlib +import json +import os +import platform +import secrets +import sqlite3 +import subprocess +import sys +from pathlib import Path +from types import SimpleNamespace + +import jwt +import pytest +from fastapi import APIRouter, FastAPI +from fastapi.security import HTTPAuthorizationCredentials +from fastapi.testclient import TestClient + +from auth import storage + + +@pytest.fixture(autouse = True) +def isolated_auth_db(tmp_path, monkeypatch): + monkeypatch.setattr(storage, "DB_PATH", tmp_path / "auth.db") + monkeypatch.setattr(storage, "_BOOTSTRAP_PW_PATH", tmp_path / ".bootstrap_password") + monkeypatch.setattr(storage, "_bootstrap_password", None) + monkeypatch.setattr(storage, "_api_key_pbkdf2_salt_cache", None) + yield + + +def seed_user(*, must_change_password = False): + storage.create_initial_user( + username = storage.DEFAULT_ADMIN_USERNAME, + password = "human-password-123", + jwt_secret = secrets.token_urlsafe(64), + must_change_password = must_change_password, + ) + + +def auth_client(): + route_path = Path(__file__).resolve().parents[1] / "routes" / "auth.py" + spec = importlib.util.spec_from_file_location("_desktop_auth_route", route_path) + auth_route = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(auth_route) + + app = FastAPI() + app.include_router(auth_route.router, prefix = "/api/auth") + return TestClient(app) + + +def data_recipe_jobs_module(): + route_path = ( + Path(__file__).resolve().parents[1] / "routes" / "data_recipe" / "jobs.py" + ) + spec = importlib.util.spec_from_file_location( + "_desktop_data_recipe_jobs", route_path + ) + jobs_route = importlib.util.module_from_spec(spec) + assert spec.loader is not None + spec.loader.exec_module(jobs_route) + return jobs_route + + +def local_recipe(): + return { + "model_providers": [{"name": "local", "is_local": True}], + "model_configs": [{"alias": "local-model", "provider": "local"}], + "columns": [{"column_type": "llm-text", "model_alias": "local-model"}], + } + + +def local_recipe_request(token): + return SimpleNamespace( + headers = {"authorization": f"Bearer {token}"}, + app = SimpleNamespace(state = SimpleNamespace(server_port = 8888)), + scope = {}, + base_url = "http://testserver/", + ) + + +@pytest.fixture +def loaded_local_model(monkeypatch): + inference_module = SimpleNamespace( + get_llama_cpp_backend = lambda: SimpleNamespace(is_loaded = True), + ) + monkeypatch.setitem(sys.modules, "routes.inference", inference_module) + + +def test_desktop_secret_round_trip_uses_real_admin_subject(): + seed_user() + raw = storage.create_desktop_secret() + + assert raw.startswith("desktop-") + assert storage.validate_desktop_secret(raw) == storage.DEFAULT_ADMIN_USERNAME + assert storage.validate_desktop_secret(raw + "x") is None + + +def test_create_desktop_secret_rotates_old_secret(): + seed_user() + old = storage.create_desktop_secret() + new = storage.create_desktop_secret() + + assert old != new + assert storage.validate_desktop_secret(old) is None + assert storage.validate_desktop_secret(new) == storage.DEFAULT_ADMIN_USERNAME + + +def test_clear_desktop_secret_invalidates_secret(): + seed_user() + raw = storage.create_desktop_secret() + + storage.clear_desktop_secret() + + assert storage.validate_desktop_secret(raw) is None + + +def test_ensure_default_admin_does_not_recreate_bootstrap_for_existing_admin(): + seed_user() + + created = storage.ensure_default_admin() + + assert created is False + assert not storage._BOOTSTRAP_PW_PATH.exists() + + +def test_ensure_default_admin_loads_existing_bootstrap_after_restart(monkeypatch): + created = storage.ensure_default_admin() + bootstrap_pw = storage._BOOTSTRAP_PW_PATH.read_text().strip() + + monkeypatch.setattr(storage, "_bootstrap_password", None) + created_again = storage.ensure_default_admin() + + assert created is True + assert storage._BOOTSTRAP_PW_PATH.exists() + assert created_again is False + assert storage.get_bootstrap_password() == bootstrap_pw + + +def test_ensure_default_admin_does_not_generate_for_empty_existing_bootstrap(): + seed_user() + storage._BOOTSTRAP_PW_PATH.write_text(" \n") + + created = storage.ensure_default_admin() + + assert created is False + assert storage._BOOTSTRAP_PW_PATH.read_text() == " \n" + assert storage.get_bootstrap_password() is None + + +def test_web_login_token_has_no_desktop_marker_and_keeps_password_gate(): + seed_user(must_change_password = True) + client = auth_client() + + response = client.post( + "/api/auth/login", + json = { + "username": storage.DEFAULT_ADMIN_USERNAME, + "password": "human-password-123", + }, + ) + + assert response.status_code == 200 + body = response.json() + assert body["must_change_password"] is True + payload = jwt.decode( + body["access_token"], + storage.get_jwt_secret(storage.DEFAULT_ADMIN_USERNAME), + algorithms = ["HS256"], + ) + assert payload["sub"] == storage.DEFAULT_ADMIN_USERNAME + assert "desktop" not in payload + + gated = client.post( + "/api/auth/api-keys", + headers = {"Authorization": f"Bearer {body['access_token']}"}, + json = {"name": "web"}, + ) + assert gated.status_code == 403 + + +def test_desktop_login_mints_admin_token_without_clearing_web_password_change(): + seed_user(must_change_password = True) + raw = storage.create_desktop_secret() + client = auth_client() + + response = client.post("/api/auth/desktop-login", json = {"secret": raw}) + + assert response.status_code == 200 + body = response.json() + assert body["access_token"] + assert body["refresh_token"] + assert body["token_type"] == "bearer" + assert body["must_change_password"] is False + assert storage.requires_password_change(storage.DEFAULT_ADMIN_USERNAME) is True + + payload = jwt.decode( + body["access_token"], + storage.get_jwt_secret(storage.DEFAULT_ADMIN_USERNAME), + algorithms = ["HS256"], + ) + assert payload["sub"] == storage.DEFAULT_ADMIN_USERNAME + assert payload["desktop"] is True + + +def test_desktop_refresh_preserves_desktop_marker(): + seed_user(must_change_password = True) + raw = storage.create_desktop_secret() + client = auth_client() + login_body = client.post("/api/auth/desktop-login", json = {"secret": raw}).json() + + response = client.post( + "/api/auth/refresh", + json = {"refresh_token": login_body["refresh_token"]}, + ) + + assert response.status_code == 200 + body = response.json() + assert body["must_change_password"] is False + payload = jwt.decode( + body["access_token"], + storage.get_jwt_secret(storage.DEFAULT_ADMIN_USERNAME), + algorithms = ["HS256"], + ) + assert payload["sub"] == storage.DEFAULT_ADMIN_USERNAME + assert payload["desktop"] is True + + +def test_desktop_session_uses_real_admin_identity_for_api_keys(): + seed_user(must_change_password = True) + raw = storage.create_desktop_secret() + client = auth_client() + token = client.post("/api/auth/desktop-login", json = {"secret": raw}).json()[ + "access_token" + ] + + response = client.post( + "/api/auth/api-keys", + headers = {"Authorization": f"Bearer {token}"}, + json = {"name": "desktop"}, + ) + + assert response.status_code == 200 + rows = storage.list_api_keys(storage.DEFAULT_ADMIN_USERNAME) + assert [row["name"] for row in rows] == ["desktop"] + + +def test_local_recipe_token_preserves_desktop_marker(loaded_local_model): + from auth.authentication import create_access_token, get_current_subject + + seed_user(must_change_password = True) + jobs_route = data_recipe_jobs_module() + incoming_token = create_access_token( + subject = storage.DEFAULT_ADMIN_USERNAME, + desktop = True, + ) + recipe = local_recipe() + + jobs_route._inject_local_providers(recipe, local_recipe_request(incoming_token)) + + local_token = recipe["model_providers"][0]["api_key"] + payload = jwt.decode( + local_token, + storage.get_jwt_secret(storage.DEFAULT_ADMIN_USERNAME), + algorithms = ["HS256"], + ) + assert payload["sub"] == storage.DEFAULT_ADMIN_USERNAME + assert payload["desktop"] is True + credentials = HTTPAuthorizationCredentials( + scheme = "Bearer", + credentials = local_token, + ) + assert ( + asyncio.run(get_current_subject(credentials)) == storage.DEFAULT_ADMIN_USERNAME + ) + + +def test_local_recipe_token_keeps_web_marker_absent(loaded_local_model): + from auth.authentication import create_access_token + + seed_user(must_change_password = False) + jobs_route = data_recipe_jobs_module() + incoming_token = create_access_token(subject = storage.DEFAULT_ADMIN_USERNAME) + recipe = local_recipe() + + jobs_route._inject_local_providers(recipe, local_recipe_request(incoming_token)) + + local_token = recipe["model_providers"][0]["api_key"] + payload = jwt.decode( + local_token, + storage.get_jwt_secret(storage.DEFAULT_ADMIN_USERNAME), + algorithms = ["HS256"], + ) + assert payload["sub"] == storage.DEFAULT_ADMIN_USERNAME + assert "desktop" not in payload + + +def test_desktop_login_rejects_invalid_secret(): + seed_user(must_change_password = False) + client = auth_client() + + response = client.post( + "/api/auth/desktop-login", + json = {"secret": "desktop-invalid"}, + ) + + assert response.status_code == 401 + + +def test_write_desktop_secret_file_is_0600_on_unix(tmp_path): + from unsloth_cli.commands import studio as studio_cli + + path = tmp_path / ".desktop_secret" + if platform.system() != "Windows": + path.write_text("old-secret") + os.chmod(path, 0o644) + + studio_cli._write_auth_secret(path, "desktop-secret") + + assert path.read_text() == "desktop-secret" + if platform.system() != "Windows": + assert oct(path.stat().st_mode & 0o777) == "0o600" + + +def test_reset_password_removes_desktop_secret_files(tmp_path, monkeypatch): + from typer.testing import CliRunner + from unsloth_cli.commands import studio as studio_cli + + auth_dir = tmp_path / "auth" + auth_dir.mkdir() + (auth_dir / "auth.db").write_text("db") + (auth_dir / ".bootstrap_password").write_text("boot") + (auth_dir / ".desktop_secret").write_text("new") + monkeypatch.setattr(studio_cli, "STUDIO_HOME", tmp_path) + + result = CliRunner().invoke(studio_cli.studio_app, ["reset-password"]) + + assert result.exit_code == 0 + assert not (auth_dir / "auth.db").exists() + assert not (auth_dir / ".bootstrap_password").exists() + assert not (auth_dir / ".desktop_secret").exists() + + +def test_reset_password_removes_desktop_secret_files_without_db(tmp_path, monkeypatch): + from typer.testing import CliRunner + from unsloth_cli.commands import studio as studio_cli + + auth_dir = tmp_path / "auth" + auth_dir.mkdir() + (auth_dir / ".desktop_secret").write_text("new") + monkeypatch.setattr(studio_cli, "STUDIO_HOME", tmp_path) + + result = CliRunner().invoke(studio_cli.studio_app, ["reset-password"]) + + assert result.exit_code == 0 + assert not (auth_dir / ".desktop_secret").exists() + + +def test_desktop_capabilities_json_reports_rollout_safe_flags(): + from typer.testing import CliRunner + import unsloth_cli.commands.studio as studio_cli + + result = CliRunner().invoke( + studio_cli.studio_app, + ["desktop-capabilities", "--json"], + ) + + assert result.exit_code == 0 + body = json.loads(result.output) + assert body["desktop_protocol_version"] == 1 + assert body["supports_provision_desktop_auth"] is True + assert body["supports_api_only"] is True + assert isinstance(body["version"], str) + + +def test_health_response_reports_desktop_capability_fields(monkeypatch): + router_stub = SimpleNamespace( + auth_router = APIRouter(), + data_recipe_router = APIRouter(), + datasets_router = APIRouter(), + export_router = APIRouter(), + inference_router = APIRouter(), + models_router = APIRouter(), + training_history_router = APIRouter(), + training_router = APIRouter(), + ) + monkeypatch.setitem(sys.modules, "routes", router_stub) + + import studio.backend.main as backend_main + + monkeypatch.setattr(backend_main._hw_module, "CHAT_ONLY", False) + + body = asyncio.run(backend_main.health_check()) + + assert body["desktop_protocol_version"] == 1 + assert body["supports_desktop_auth"] is True + + +def test_provision_desktop_auth_writes_secret_and_creates_db_without_backend_deps( + tmp_path, + monkeypatch, +): + auth_dir = tmp_path / "auth" + auth_dir.mkdir() + + code = """ +import builtins +import sys +from pathlib import Path +from typer.testing import CliRunner + +studio_home = Path(sys.argv[1]) +real_import = builtins.__import__ + +def guarded_import(name, *args, **kwargs): + blocked = ("auth", "fastapi", "structlog", "utils") + if name in blocked or name.startswith(("auth.", "utils.")): + raise ModuleNotFoundError(name) + return real_import(name, *args, **kwargs) + +builtins.__import__ = guarded_import +from unsloth_cli.commands import studio as studio_cli + +studio_cli.STUDIO_HOME = studio_home +result = CliRunner().invoke(studio_cli.studio_app, ["provision-desktop-auth"]) +if result.exit_code != 0: + print(result.output) + if result.exception is not None: + raise result.exception + raise SystemExit(result.exit_code) +""" + result = subprocess.run( + [sys.executable, "-c", code, str(tmp_path)], + cwd = Path(__file__).resolve().parents[3], + env = {**os.environ, "PYTHONPATH": "."}, + text = True, + capture_output = True, + ) + assert result.returncode == 0, result.stderr + result.stdout + secret = (auth_dir / ".desktop_secret").read_text() + assert secret.startswith("desktop-") + + conn = sqlite3.connect(auth_dir / "auth.db") + conn.row_factory = sqlite3.Row + try: + user = conn.execute( + """ + SELECT username, password_salt, password_hash, must_change_password + FROM auth_user + """ + ).fetchone() + app_secrets = { + row["key"]: row["value"] + for row in conn.execute("SELECT key, value FROM app_secrets") + } + refresh_columns = { + row["name"] for row in conn.execute("PRAGMA table_info(refresh_tokens)") + } + finally: + conn.close() + + bootstrap_password = (auth_dir / ".bootstrap_password").read_text().strip() + bootstrap_hash = hashlib.pbkdf2_hmac( + "sha256", + bootstrap_password.encode("utf-8"), + user["password_salt"].encode("utf-8"), + 100_000, + ).hex() + + assert bootstrap_password + assert user["username"] == "unsloth" + assert user["must_change_password"] == 1 + assert bootstrap_hash == user["password_hash"] + assert len(app_secrets["api_key_pbkdf2_salt"]) == 64 + assert len(app_secrets["desktop_secret_hash"]) == 64 + assert app_secrets["desktop_secret_created_at"] + assert "is_desktop" in refresh_columns + + monkeypatch.setattr(storage, "DB_PATH", auth_dir / "auth.db") + monkeypatch.setattr(storage, "_api_key_pbkdf2_salt_cache", None) + assert storage.validate_desktop_secret(secret) == storage.DEFAULT_ADMIN_USERNAME + assert storage.requires_password_change(storage.DEFAULT_ADMIN_USERNAME) is True + + +def test_provision_desktop_auth_keeps_existing_admin_password(tmp_path, monkeypatch): + from typer.testing import CliRunner + from unsloth_cli.commands import studio as studio_cli + + auth_dir = tmp_path / "auth" + auth_dir.mkdir() + monkeypatch.setattr(studio_cli, "STUDIO_HOME", tmp_path) + + conn = sqlite3.connect(auth_dir / "auth.db") + try: + conn.execute( + """ + CREATE TABLE auth_user ( + id INTEGER PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_salt TEXT NOT NULL, + password_hash TEXT NOT NULL, + jwt_secret TEXT NOT NULL, + must_change_password INTEGER NOT NULL DEFAULT 0 + ) + """ + ) + conn.execute( + """ + INSERT INTO auth_user ( + username, password_salt, password_hash, jwt_secret, must_change_password + ) + VALUES (?, ?, ?, ?, ?) + """, + ("unsloth", "existing-salt", "existing-hash", "existing-jwt", 0), + ) + conn.commit() + finally: + conn.close() + + result = CliRunner().invoke(studio_cli.studio_app, ["provision-desktop-auth"]) + + assert result.exit_code == 0 + assert not (auth_dir / ".bootstrap_password").exists() + conn = sqlite3.connect(auth_dir / "auth.db") + conn.row_factory = sqlite3.Row + try: + user = conn.execute( + """ + SELECT password_salt, password_hash, jwt_secret, must_change_password + FROM auth_user WHERE username = ? + """, + ("unsloth",), + ).fetchone() + finally: + conn.close() + + assert dict(user) == { + "password_salt": "existing-salt", + "password_hash": "existing-hash", + "jwt_secret": "existing-jwt", + "must_change_password": 0, + } + + +def test_update_password_clears_desktop_secret(): + seed_user() + raw = storage.create_desktop_secret() + assert storage.validate_desktop_secret(raw) == storage.DEFAULT_ADMIN_USERNAME + + changed = storage.update_password( + storage.DEFAULT_ADMIN_USERNAME, "new-admin-password" + ) + assert changed is True + assert storage.validate_desktop_secret(raw) is None + + +def test_update_password_on_unknown_user_leaves_desktop_secret_intact(): + seed_user() + raw = storage.create_desktop_secret() + + changed = storage.update_password("not-a-user", "irrelevant") + assert changed is False + assert storage.validate_desktop_secret(raw) == storage.DEFAULT_ADMIN_USERNAME + + +def test_desktop_auth_provision_has_bounded_timeout(): + rs_path = ( + Path(__file__).resolve().parents[3] + / "studio" + / "src-tauri" + / "src" + / "desktop_auth.rs" + ) + src = rs_path.read_text() + start = src.index("async fn provision_desktop_auth(") + depth = 0 + body_start = src.index("{", start) + body_end = None + for i in range(body_start, len(src)): + c = src[i] + if c == "{": + depth += 1 + elif c == "}": + depth -= 1 + if depth == 0: + body_end = i + 1 + break + assert body_end is not None + body = src[start:body_end] + assert "tokio::time::timeout" in body + import re + + m = re.search(r"Duration::from_secs\(\s*(\d+)\s*\)", body) + assert m is not None + seconds = int(m.group(1)) + assert 5 <= seconds <= 120 diff --git a/studio/backend/utils/hardware/nvidia.py b/studio/backend/utils/hardware/nvidia.py index dc5295c302..274d9beb48 100644 --- a/studio/backend/utils/hardware/nvidia.py +++ b/studio/backend/utils/hardware/nvidia.py @@ -6,6 +6,10 @@ from typing import Any, Optional from loggers import get_logger +from utils.subprocess_compat import ( + windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, +) + logger = get_logger(__name__) @@ -61,6 +65,7 @@ def get_physical_gpu_count() -> Optional[int]: capture_output = True, text = True, timeout = 5, + **_windows_hidden_subprocess_kwargs(), ) if result.returncode == 0 and result.stdout.strip(): return len(result.stdout.strip().splitlines()) @@ -85,6 +90,7 @@ def get_primary_gpu_utilization() -> dict[str, Any]: capture_output = True, text = True, timeout = 5, + **_windows_hidden_subprocess_kwargs(), ) except (OSError, subprocess.TimeoutExpired) as e: logger.warning("nvidia-smi query failed in get_primary_gpu_utilization: %s", e) @@ -135,6 +141,7 @@ def get_visible_gpu_utilization( capture_output = True, text = True, timeout = 5, + **_windows_hidden_subprocess_kwargs(), ) except (OSError, subprocess.TimeoutExpired) as e: logger.warning("nvidia-smi query failed in get_visible_gpu_utilization: %s", e) @@ -220,6 +227,7 @@ def get_backend_visible_gpu_info( capture_output = True, text = True, timeout = 10, + **_windows_hidden_subprocess_kwargs(), ) except (OSError, subprocess.TimeoutExpired) as e: logger.warning("nvidia-smi query failed in get_backend_visible_gpu_info: %s", e) diff --git a/studio/backend/utils/models/model_config.py b/studio/backend/utils/models/model_config.py index a2d48cf009..a2b0c90e59 100644 --- a/studio/backend/utils/models/model_config.py +++ b/studio/backend/utils/models/model_config.py @@ -32,6 +32,10 @@ import threading import yaml +from utils.subprocess_compat import ( + windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, +) + logger = get_logger(__name__) # ── Model size extraction ──────────────────────────────────── @@ -579,6 +583,7 @@ def _is_vision_model_subprocess( capture_output = True, text = True, timeout = 60, + **_windows_hidden_subprocess_kwargs(), ) if result.returncode != 0: diff --git a/studio/backend/utils/subprocess_compat.py b/studio/backend/utils/subprocess_compat.py new file mode 100644 index 0000000000..bedf8cf2e6 --- /dev/null +++ b/studio/backend/utils/subprocess_compat.py @@ -0,0 +1,34 @@ +# 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 subprocess helpers for the Unsloth Studio backend.""" + +import subprocess +import sys + + +def windows_hidden_subprocess_kwargs() -> dict[str, object]: + """Return Windows-only subprocess kwargs that suppress console windows. + + On non-Windows platforms returns an empty dict so callers can always + unpack the result into ``subprocess.run`` / ``subprocess.Popen`` via + ``**windows_hidden_subprocess_kwargs()``. + """ + if sys.platform != "win32": + return {} + + kwargs: dict[str, object] = {} + create_no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0) + if create_no_window: + kwargs["creationflags"] = create_no_window + + startupinfo_factory = getattr(subprocess, "STARTUPINFO", None) + startf_use_showwindow = getattr(subprocess, "STARTF_USESHOWWINDOW", 0) + sw_hide = getattr(subprocess, "SW_HIDE", 0) + if startupinfo_factory is not None and startf_use_showwindow: + startupinfo = startupinfo_factory() + startupinfo.dwFlags |= startf_use_showwindow + startupinfo.wShowWindow = sw_hide + kwargs["startupinfo"] = startupinfo + + return kwargs diff --git a/studio/backend/utils/transformers_version.py b/studio/backend/utils/transformers_version.py index 36c3a4c22d..f36bdcd6e8 100644 --- a/studio/backend/utils/transformers_version.py +++ b/studio/backend/utils/transformers_version.py @@ -36,6 +36,10 @@ import subprocess import sys from pathlib import Path +from utils.subprocess_compat import ( + windows_hidden_subprocess_kwargs as _windows_hidden_subprocess_kwargs, +) + logger = get_logger(__name__) @@ -499,6 +503,7 @@ def _install_to_dir(pkg: str, target_dir: str) -> bool: stdout = subprocess.PIPE, stderr = subprocess.STDOUT, text = True, + **_windows_hidden_subprocess_kwargs(), ) if result.returncode == 0: return True @@ -520,6 +525,7 @@ def _install_to_dir(pkg: str, target_dir: str) -> bool: stdout = subprocess.PIPE, stderr = subprocess.STDOUT, text = True, + **_windows_hidden_subprocess_kwargs(), ) if result.returncode != 0: logger.error("install failed:\n%s", result.stdout) diff --git a/studio/frontend/package.json b/studio/frontend/package.json index a2eebd5cb5..c5cb949ccd 100644 --- a/studio/frontend/package.json +++ b/studio/frontend/package.json @@ -41,6 +41,10 @@ "@tailwindcss/vite": "^4.2.2", "@tanstack/react-router": "^1.159.10", "@tanstack/react-table": "^8.21.3", + "@tauri-apps/api": "^2.10.1", + "@tauri-apps/plugin-opener": "^2.5.3", + "@tauri-apps/plugin-process": "^2.3.1", + "@tauri-apps/plugin-updater": "^2.10.1", "@toolwind/corner-shape": "^0.0.8-3", "@types/canvas-confetti": "^1.9.0", "@xyflow/react": "^12.10.0", diff --git a/studio/frontend/public/studio.png b/studio/frontend/public/studio.png new file mode 100644 index 0000000000000000000000000000000000000000..4e531499b7cf115cfa016e1b3976f6741fa6f0a2 GIT binary patch literal 27420 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 { @@ -16,55 +18,64 @@ async function hasActiveSession(): Promise { return refreshSession(); } -async function checkAuthInitialized(): Promise { +interface AuthStatus { + initialized: boolean; + requires_password_change: boolean; +} + +async function fetchAuthStatus(): Promise { try { - const res = await fetch("/api/auth/status"); - if (!res.ok) return true; // fallback to login on error - const data = (await res.json()) as { initialized: boolean }; - return data.initialized; + const res = await fetch(apiUrl("/api/auth/status")); + if (!res.ok) return { initialized: true, requires_password_change: mustChangePassword() }; + return (await res.json()) as AuthStatus; } catch { - return true; // fallback to login on error + return { initialized: true, requires_password_change: mustChangePassword() }; } } -async function checkPasswordChangeRequired(): Promise { - try { - const res = await fetch("/api/auth/status"); - if (!res.ok) return mustChangePassword(); - const data = (await res.json()) as { requires_password_change: boolean }; - return data.requires_password_change || mustChangePassword(); - } catch { - return mustChangePassword(); - } +function authRedirect(to: "/login" | "/change-password"): never { + throw redirect({ to }); } export async function requireAuth(): Promise { + if (isTauri) { + await tauriAutoAuth(); + return; + } + if (await hasActiveSession()) { - if (await checkPasswordChangeRequired()) { - throw redirect({ to: "/change-password" }); + const { requires_password_change } = await fetchAuthStatus(); + if (requires_password_change || mustChangePassword()) { + authRedirect("/change-password"); } return; } - const requiresPasswordChange = await checkPasswordChangeRequired(); - if (requiresPasswordChange) throw redirect({ to: "/change-password" }); - const initialized = await checkAuthInitialized(); - throw redirect({ to: initialized ? "/login" : "/change-password" }); + const status = await fetchAuthStatus(); + if (status.requires_password_change || mustChangePassword()) { + authRedirect("/change-password"); + } + authRedirect(status.initialized ? "/login" : "/change-password"); } export async function requireGuest(): Promise { + if (isTauri) { + await tauriAutoAuth(); + throw redirect({ to: "/chat" }); + } if (!(await hasActiveSession())) return; throw redirect({ to: getPostAuthRoute() }); } export async function requirePasswordChangeFlow(): Promise { - const requiresPasswordChange = await checkPasswordChangeRequired(); - - if (requiresPasswordChange) return; + if (isTauri) { + await tauriAutoAuth(); + throw redirect({ to: "/chat" }); + } + const status = await fetchAuthStatus(); + if (status.requires_password_change || mustChangePassword()) return; if (await hasActiveSession()) { throw redirect({ to: getPostAuthRoute() }); } - - const initialized = await checkAuthInitialized(); - throw redirect({ to: initialized ? "/login" : "/change-password" }); + authRedirect(status.initialized ? "/login" : "/change-password"); } diff --git a/studio/frontend/src/app/provider.tsx b/studio/frontend/src/app/provider.tsx index 68ce3061bd..b75998a169 100644 --- a/studio/frontend/src/app/provider.tsx +++ b/studio/frontend/src/app/provider.tsx @@ -1,18 +1,181 @@ // 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 { StartupScreen } from "@/components/tauri/startup-screen"; +import { UpdateBanner } from "@/components/tauri/update-banner"; +import { UpdateScreen } from "@/components/tauri/update-screen"; import { Toaster } from "@/components/ui/sonner"; +import { useTauriBackend } from "@/hooks/use-tauri-backend"; +import { useTauriUpdate } from "@/hooks/use-tauri-update"; +import { isTauri } from "@/lib/api-base"; import { ThemeProvider } from "next-themes"; -import type { ReactNode } from "react"; +import { useEffect, useRef, type ReactNode } from "react"; interface AppProviderProps { children: ReactNode; } +// --------------------------------------------------------------------------- +// Tauri window helpers (only imported in Tauri mode) +// --------------------------------------------------------------------------- + +async function showWindow(): Promise { + const { getCurrentWindow } = await import("@tauri-apps/api/window"); + await getCurrentWindow().show(); +} + +function easeOutQuart(t: number): number { + return 1 - (1 - t) ** 4; +} + +async function animateToGoldenRatio(abortRef: { current: boolean }): Promise { + const { getCurrentWindow, currentMonitor, LogicalSize } = await import("@tauri-apps/api/window"); + const win = getCurrentWindow(); + + // Ensure window is visible before resizing + await win.show(); + + const monitor = await currentMonitor(); + if (!monitor) return; + + // Convert physical pixels to logical using scale factor + const scale = monitor.scaleFactor; + const screenW = monitor.size.width / scale; + const screenH = monitor.size.height / scale; + + // Target: 75% of screen width, golden ratio height, capped at min 900x600 + const targetW = Math.max(900, Math.round(screenW * 0.75)); + const targetH = Math.max(600, Math.round(targetW / 1.618)); + // Don't exceed screen height + const finalH = Math.min(targetH, Math.round(screenH * 0.85)); + const finalW = targetW; + + // Check reduced motion preference + const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + if (prefersReducedMotion) { + await win.setSize(new LogicalSize(finalW, finalH)); + } else { + // Read current size instead of hardcoding — stays correct if tauri.conf.json changes + const inner = await win.innerSize(); + const factor = await win.scaleFactor(); + const startW = Math.round(inner.width / factor); + const startH = Math.round(inner.height / factor); + const steps = 15; + const stepDuration = 23; // ~350ms total + + for (let i = 1; i <= steps; i++) { + if (abortRef.current) return; + const t = easeOutQuart(i / steps); + const w = Math.round(startW + (finalW - startW) * t); + const h = Math.round(startH + (finalH - startH) * t); + await win.setSize(new LogicalSize(w, h)); + await new Promise((r) => setTimeout(r, stepDuration)); + } + } + + if (abortRef.current) return; + + // Apply constraints and finalize + await win.setResizable(true); + await win.setSizeConstraints({ minWidth: 900, minHeight: 600 }); + await win.center(); +} + +// --------------------------------------------------------------------------- +// TauriWrapper +// --------------------------------------------------------------------------- + +function TauriUpdateLayer({ isExternalServer }: { isExternalServer: boolean }) { + const update = useTauriUpdate(isExternalServer); + const isUpdating = + update.status === "updating-backend" || + update.status === "downloading" || + update.status === "installing" || + (update.status === "error" && !update.dismissed); + + if (isUpdating) { + return ( + + ); + } + + return ( + + ); +} + +function TauriWrapper({ children }: { children: ReactNode }) { + const { + status, logs, error, isExternalServer, + currentStepIndex, progressDetail, elevationPackages, + startInstall, retry, retryInstall, approveElevation, + } = useTauriBackend(); + + const hasResized = useRef(false); + const abortRef = useRef(false); + + // Show the window once the frontend mounts (for pre-running states) + useEffect(() => { + if (isTauri) void showWindow(); + }, []); + + // Animate resize when backend becomes ready + useEffect(() => { + if (status === "running" && !hasResized.current) { + hasResized.current = true; + abortRef.current = false; + animateToGoldenRatio(abortRef).catch(async () => { + // On failure, at minimum make the window resizable so user can fix manually + try { + const { getCurrentWindow } = await import("@tauri-apps/api/window"); + await getCurrentWindow().setResizable(true); + } catch { /* swallow — window may still be functional */ } + }); + } + return () => { abortRef.current = true; }; + }, [status]); + + if (!isTauri) return <>{children}; + if (status === "running") return <>{children}; + + return ( + + ); +} + export function AppProvider({ children }: AppProviderProps) { return ( - {children} + + {children} + ); diff --git a/studio/frontend/src/app/routes/__root.tsx b/studio/frontend/src/app/routes/__root.tsx index 69aeb11748..19b5763557 100644 --- a/studio/frontend/src/app/routes/__root.tsx +++ b/studio/frontend/src/app/routes/__root.tsx @@ -3,8 +3,8 @@ import { AppSidebar } from "@/components/app-sidebar"; import { Navbar } from "@/components/navbar"; +import { fetchDeviceType, usePlatformStore } from "@/config/env"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; -import { usePlatformStore } from "@/config/env"; import { SettingsDialog, useSettingsDialogStore } from "@/features/settings"; import { useTrainingUnloadGuard } from "@/features/training/hooks/use-training-unload-guard"; import { useSidebarPin } from "@/hooks/use-sidebar-pin"; @@ -33,7 +33,10 @@ function isChatOnlyAllowed(pathname: string): boolean { } export const Route = createRootRoute({ - beforeLoad: ({ location }) => { + beforeLoad: async ({ location }) => { + // Ensure platform info is fetched before checking chat-only guard. + // fetchDeviceType caches after first call, so subsequent navigations are instant. + await fetchDeviceType(); const chatOnly = usePlatformStore.getState().isChatOnly(); if (chatOnly && !isChatOnlyAllowed(location.pathname)) { throw redirect({ to: "/chat" }); diff --git a/studio/frontend/src/components/assistant-ui/markdown-text.tsx b/studio/frontend/src/components/assistant-ui/markdown-text.tsx index 2a0517a44a..7eb4b21ba7 100644 --- a/studio/frontend/src/components/assistant-ui/markdown-text.tsx +++ b/studio/frontend/src/components/assistant-ui/markdown-text.tsx @@ -5,6 +5,7 @@ import { copyToClipboard } from "@/lib/copy-to-clipboard"; import { preprocessLaTeX } from "@/lib/latex"; +import { openLink } from "@/lib/open-link"; import { INTERNAL, useMessagePartText } from "@assistant-ui/react"; import { Copy02Icon, Tick02Icon } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; @@ -32,9 +33,13 @@ const STREAMDOWN_COMPONENTS = { }: React.ComponentProps<"a">) => ( { + if (href && openLink(href)) { + e.preventDefault(); + } + }} {...props} > {children} diff --git a/studio/frontend/src/components/assistant-ui/sources.tsx b/studio/frontend/src/components/assistant-ui/sources.tsx index da97ff66b5..81c8b0c213 100644 --- a/studio/frontend/src/components/assistant-ui/sources.tsx +++ b/studio/frontend/src/components/assistant-ui/sources.tsx @@ -1,5 +1,6 @@ "use client"; +import { openLink } from "@/lib/open-link"; import { memo, useState, @@ -93,8 +94,8 @@ function Source({ variant, size, asChild = false, - target = "_blank", - rel = "noopener noreferrer", + href, + onClick, ...props }: SourceProps) { return ( @@ -109,8 +110,14 @@ function Source({ > { + if (href && openLink(href)) { + e.preventDefault(); + } + onClick?.(e); + }} {...(props as ComponentProps<"a">)} /> diff --git a/studio/frontend/src/components/tauri/startup-screen.tsx b/studio/frontend/src/components/tauri/startup-screen.tsx new file mode 100644 index 0000000000..af5f241416 --- /dev/null +++ b/studio/frontend/src/components/tauri/startup-screen.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 { ShimmerButton } from "@/components/ui/shimmer-button"; +import type { BackendStatus } from "@/hooks/use-tauri-backend"; +import { AnimatePresence, motion } from "motion/react"; + +interface StartupScreenProps { + status: BackendStatus; + logs: string[]; + error: string | null; + currentStepIndex: number; + progressDetail: string | null; + elevationPackages: string[]; + onInstall: () => void; + onRetry: () => void; + onRetryInstall: () => void; + onApproveElevation: () => void; + onStartServer: () => void; +} + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const INSTALL_STEPS = [ + "Detecting your system", + "Checking dependencies", + "Setting up package manager", + "Creating Python environment", + "Installing ML framework", + "Installing Unsloth", + "Finalizing setup", +] as const; + +const EASE_OUT_QUART: [number, number, number, number] = [0.165, 0.84, 0.44, 1]; + +// --------------------------------------------------------------------------- +// Sub-components +// --------------------------------------------------------------------------- + +function TealSpinner({ size = 24 }: { size?: number }) { + return ( + + ); +} + +function Logo() { + return ( +
+ Unsloth + Unsloth Studio +
+ ); +} + +function ActionButton({ + onClick, + variant = "primary", + children, +}: { + onClick: () => void; + variant?: "primary" | "secondary"; + children: React.ReactNode; +}) { + const base = "rounded-lg px-5 py-2.5 text-sm font-medium cursor-pointer transition-colors"; + const styles = + variant === "primary" + ? `${base} bg-primary text-primary-foreground hover:bg-primary/80` + : `${base} bg-muted text-foreground hover:bg-muted/80`; + return ( + + ); +} + +// --------------------------------------------------------------------------- +// Per-status renderers +// --------------------------------------------------------------------------- + +function CheckingContent() { + return ( +
+
+ +
+
+ +

Checking...

+
+
+ ); +} + +function NotInstalledContent({ onInstall }: { onInstall: () => void }) { + return ( +
+
+ +

+ To install Unsloth, click Get Started. +

+
+
+ + Get Started + +
+
+ ); +} + +function InstallingContent({ + currentStepIndex, + progressDetail, +}: { + currentStepIndex: number; + progressDetail: string | null; +}) { + const stepNum = Math.max(0, currentStepIndex) + 1; + const stepLabel = INSTALL_STEPS[Math.min(currentStepIndex, INSTALL_STEPS.length - 1)]; + + return ( +
+
+ +
+
+ +

Installing...

+

+ Please wait a few mins, then you can start training. +

+ {currentStepIndex >= 0 && ( +

+ Step {stepNum} of {INSTALL_STEPS.length}: {stepLabel} +

+ )} + {progressDetail && ( +

{progressDetail}

+ )} +
+
+ ); +} + +function RepairingContent({ + logs, + progressDetail, +}: { + logs: string[]; + progressDetail: string | null; +}) { + const latest = progressDetail ?? logs.at(-1); + + return ( +
+
+ +
+
+ +

Updating existing Studio install...

+ {latest && ( +

{latest}

+ )} +
+
+ ); +} + +function InstallErrorContent({ + error, + logs, + onRetryInstall, +}: { + error: string | null; + logs: string[]; + onRetryInstall: () => void; +}) { + return ( + <> + +
+

Setup ran into a problem

+ {error && ( +

{error}

+ )} +
+ void navigator.clipboard.writeText(logs.join("\n"))} + > + Copy Logs + + Try Again +
+
+ + ); +} + +function RepairErrorContent({ + error, + logs, + onRetry, +}: { + error: string | null; + logs: string[]; + onRetry: () => void; +}) { + return ( + <> + +
+

Update failed

+ {error && ( +

{error}

+ )} +
+ void navigator.clipboard.writeText(logs.join("\n"))} + > + Copy Logs + + Retry +
+
+ + ); +} + +function NeedsElevationContent({ + elevationPackages, + onApproveElevation, + onRetryInstall, +}: { + elevationPackages: string[]; + onApproveElevation: () => void; + onRetryInstall: () => void; +}) { + return ( + <> + +
+

Permission needed

+

+ The following system packages need to be installed: +

+
+ {elevationPackages.map((pkg) => ( +
{pkg}
+ ))} +
+
+ Cancel + Allow +
+
+ + ); +} + +function StartingContent() { + return ( +
+
+ +
+
+ +

Starting server...

+
+
+ ); +} + +function StoppedContent({ onStartServer }: { onStartServer: () => void }) { + return ( + <> + +
+

Server stopped

+
+ Start Server +
+
+ + ); +} + +function ErrorContent({ + error, + logs, + onRetry, +}: { + error: string | null; + logs: string[]; + onRetry: () => void; +}) { + return ( + <> + +
+

Something went wrong

+ {error && ( +

{error}

+ )} +
+ void navigator.clipboard.writeText(logs.join("\n"))} + > + Copy Logs + + Retry +
+
+ + ); +} + +// --------------------------------------------------------------------------- +// Main component +// --------------------------------------------------------------------------- + +export function StartupScreen({ + status, + logs, + error, + currentStepIndex, + progressDetail, + elevationPackages, + onInstall, + onRetry, + onRetryInstall, + onApproveElevation, + onStartServer, +}: StartupScreenProps) { + function renderContent() { + switch (status) { + case "checking": + return ; + case "not-installed": + return ; + case "installing": + return ; + case "install-error": + return ; + case "repairing": + return ; + case "repair-error": + return ; + case "needs-elevation": + return ( + + ); + case "starting": + return ; + case "running": + return null; + case "stopped": + return ; + case "error": + return ; + } + } + + return ( +
+
+ + + {renderContent()} + + +
+
+ ); +} diff --git a/studio/frontend/src/components/tauri/update-banner.tsx b/studio/frontend/src/components/tauri/update-banner.tsx new file mode 100644 index 0000000000..98652c4db1 --- /dev/null +++ b/studio/frontend/src/components/tauri/update-banner.tsx @@ -0,0 +1,84 @@ +// 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 { Button } from "@/components/ui/button"; +import type { UpdateInfo, UpdateStatus } from "@/hooks/use-tauri-update"; +import { AnimatePresence, motion } from "motion/react"; + +interface UpdateBannerProps { + status: UpdateStatus; + info: UpdateInfo | null; + dismissed: boolean; + isExternalServer?: boolean; + onInstall: () => void; + onDismiss: () => void; +} + +const EASE_OUT_QUART: [number, number, number, number] = [0.165, 0.84, 0.44, 1]; + +export function UpdateBanner({ + status, + info, + dismissed, + isExternalServer = false, + onInstall, + onDismiss, +}: UpdateBannerProps) { + const visible = status === "available"; + const show = visible && !dismissed; + + return ( + + {show && info && ( + +
+ {/* Close button */} + + + {/* Header */} +
+ 🦥 +
+

+ New version: v{info.version} +

+

+ {isExternalServer + ? "Run `unsloth studio update` from your terminal" + : "A new app update is available"} +

+
+
+ + {/* Actions */} +
+ + + +
+
+
+ )} +
+ ); +} diff --git a/studio/frontend/src/components/tauri/update-screen.tsx b/studio/frontend/src/components/tauri/update-screen.tsx new file mode 100644 index 0000000000..a8b77ab1fa --- /dev/null +++ b/studio/frontend/src/components/tauri/update-screen.tsx @@ -0,0 +1,173 @@ +// 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 { UpdateStatus } from "@/hooks/use-tauri-update"; +import { AnimatePresence, motion } from "motion/react"; +import { useEffect, useRef } from "react"; + +interface UpdateScreenProps { + status: UpdateStatus; + logs: string[]; + progress: number; + error: string | null; + onRetry: () => void; + onSkipRestart: () => void; +} + +const EASE_OUT_QUART: [number, number, number, number] = [0.165, 0.84, 0.44, 1]; + +function Spinner({ size = 24 }: { size?: number }) { + return ( + + ); +} + +function Logo() { + return ( +
+ Unsloth + Unsloth Studio +
+ ); +} + +function statusLabel(status: UpdateStatus): string { + switch (status) { + case "updating-backend": + return "Updating backend..."; + case "downloading": + return "Downloading app update..."; + case "installing": + return "Installing update..."; + case "error": + return "Update failed"; + default: + return "Updating..."; + } +} + +function statusSubtext(status: UpdateStatus, progress: number): string { + switch (status) { + case "updating-backend": + return "This may take a few minutes. Do not close the app."; + case "downloading": + return `${progress}% downloaded`; + case "installing": + return "The app will restart shortly."; + case "error": + return "Something went wrong during the update."; + default: + return ""; + } +} + +function LogViewer({ logs }: { logs: string[] }) { + const scrollRef = useRef(null); + + useEffect(() => { + if (scrollRef.current) { + scrollRef.current.scrollTop = scrollRef.current.scrollHeight; + } + }, [logs]); + + if (logs.length === 0) return null; + + return ( +
+ {logs.map((line, i) => ( +
+ {line} +
+ ))} +
+ ); +} + +export function UpdateScreen({ + status, + logs, + progress, + error, + onRetry, + onSkipRestart, +}: UpdateScreenProps) { + const isError = status === "error"; + + return ( +
+ + + +
+ {!isError && } +

+ {statusLabel(status)} +

+

+ {statusSubtext(status, progress)} +

+
+ + {/* Download progress bar */} + {status === "downloading" && ( +
+ +
+ )} + + {/* Error display */} + + {isError && error && ( + +

{error}

+
+ )} +
+ + {/* Error actions */} + {isError && ( +
+ + +
+ )} + + {/* Log viewer */} + +
+
+ ); +} diff --git a/studio/frontend/src/components/ui/shimmer-button.tsx b/studio/frontend/src/components/ui/shimmer-button.tsx new file mode 100644 index 0000000000..d675cc0979 --- /dev/null +++ b/studio/frontend/src/components/ui/shimmer-button.tsx @@ -0,0 +1,96 @@ +import React, { type ComponentPropsWithoutRef, type CSSProperties } from "react" + +import { cn } from "@/lib/utils" + +export interface ShimmerButtonProps extends ComponentPropsWithoutRef<"button"> { + shimmerColor?: string + shimmerSize?: string + borderRadius?: string + shimmerDuration?: string + background?: string + className?: string + children?: React.ReactNode +} + +export const ShimmerButton = React.forwardRef< + HTMLButtonElement, + ShimmerButtonProps +>( + ( + { + shimmerColor = "#ffffff", + shimmerSize = "0.05em", + shimmerDuration = "3s", + borderRadius = "100px", + background = "rgba(0, 0, 0, 1)", + className, + children, + ...props + }, + ref + ) => { + return ( + + ) + } +) + +ShimmerButton.displayName = "ShimmerButton" diff --git a/studio/frontend/src/config/env.ts b/studio/frontend/src/config/env.ts index 91e17f6bb9..72bb3fa815 100644 --- a/studio/frontend/src/config/env.ts +++ b/studio/frontend/src/config/env.ts @@ -1,6 +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 { apiUrl } from "@/lib/api-base"; import { create } from "zustand"; export const env = { @@ -21,9 +22,21 @@ interface PlatformState { isChatOnly: () => boolean; } +// Client-side platform detection as fallback when backend isn't ready yet. +function detectLocalPlatform(): DeviceType { + if (typeof navigator === "undefined") return "linux"; + const platform = navigator.platform.toLowerCase(); + const ua = navigator.userAgent.toLowerCase(); + if (platform.includes("mac") || ua.includes("mac")) return "mac"; + if (platform.includes("win") || ua.includes("win")) return "windows"; + return "linux"; +} + +const localDeviceType = detectLocalPlatform(); + export const usePlatformStore = create()((_, get) => ({ - deviceType: "linux", - chatOnly: false, + deviceType: localDeviceType, + chatOnly: localDeviceType === "mac", fetched: false, isChatOnly: () => get().chatOnly, })); @@ -33,16 +46,22 @@ export async function fetchDeviceType(): Promise { if (fetched) return usePlatformStore.getState().deviceType; try { - const res = await fetch("/api/health"); + const res = await fetch(apiUrl("/api/health")); if (res.ok) { const data = (await res.json()) as { device_type?: string; chat_only?: boolean }; - const deviceType = data.device_type ?? "linux"; + const deviceType = data.device_type ?? detectLocalPlatform(); const chatOnly = data.chat_only ?? deviceType === "mac"; usePlatformStore.setState({ deviceType, chatOnly, fetched: true }); return deviceType; } - } catch (err) { - console.warn("[platform] Failed to fetch device type, will retry", err); + } catch { + // Backend not ready — use client-side detection so chat-only guard + // still works on initial load (important for macOS). Keep fetched=false + // so a later call retries against the backend. + const deviceType = detectLocalPlatform(); + const chatOnly = deviceType === "mac"; + usePlatformStore.setState({ deviceType, chatOnly, fetched: false }); + return deviceType; } return usePlatformStore.getState().deviceType; diff --git a/studio/frontend/src/features/auth/api.ts b/studio/frontend/src/features/auth/api.ts index 3bb2c9139c..97d815950d 100644 --- a/studio/frontend/src/features/auth/api.ts +++ b/studio/frontend/src/features/auth/api.ts @@ -1,6 +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 { apiUrl, isTauri } from "@/lib/api-base"; import { clearAuthTokens, getAuthToken, @@ -34,7 +35,7 @@ async function redirectToAuth(): Promise { let target = "/login"; try { - const res = await fetch("/api/auth/status"); + const res = await fetch(apiUrl("/api/auth/status")); if (res.ok) { const data = (await res.json()) as { requires_password_change: boolean }; if (data.requires_password_change || mustChangePassword()) target = "/change-password"; @@ -46,12 +47,32 @@ async function redirectToAuth(): Promise { window.location.href = target; } +async function retryWithCurrentToken( + input: RequestInfo | URL, + init?: RequestInit, +): Promise { + const retryHeaders = new Headers(init?.headers); + const token = getAuthToken(); + if (token) retryHeaders.set("Authorization", `Bearer ${token}`); + return fetch(input, { ...init, headers: retryHeaders }); +} + +async function retryWithTauriAutoAuth( + input: RequestInfo | URL, + init?: RequestInit, +): Promise { + clearAuthTokens(); + const { tauriAutoAuth } = await import("./tauri-auto-auth"); + if (await tauriAutoAuth()) return retryWithCurrentToken(input, init); + return null; +} + export async function refreshSession(): Promise { const refreshToken = getRefreshToken(); if (!refreshToken) return false; try { - const response = await fetch("/api/auth/refresh", { + const response = await fetch(apiUrl("/api/auth/refresh"), { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: refreshToken }), @@ -78,6 +99,7 @@ export async function authFetch( input: RequestInfo | URL, init?: RequestInit, ): Promise { + const resolvedInput = typeof input === 'string' ? apiUrl(input) : input; const headers = new Headers(init?.headers); const accessToken = getAuthToken(); if (accessToken) { @@ -86,14 +108,18 @@ export async function authFetch( let response: Response; try { - response = await fetch(input, { ...init, headers }); + response = await fetch(resolvedInput, { ...init, headers }); } catch (err) { if (err instanceof TypeError) { throw new Error("Studio isn't running -- please relaunch it."); } throw err; } + if (await isPasswordChangeRequiredResponse(response)) { + if (isTauri) { + return (await retryWithTauriAutoAuth(resolvedInput, init)) ?? response; + } void redirectToAuth(); return response; } @@ -101,25 +127,24 @@ export async function authFetch( const refreshed = await refreshSession(); if (!refreshed) { + if (isTauri) { + return (await retryWithTauriAutoAuth(resolvedInput, init)) ?? response; + } clearAuthTokens(); void redirectToAuth(); return response; } if (mustChangePassword()) { + if (isTauri) { + return (await retryWithTauriAutoAuth(resolvedInput, init)) ?? response; + } void redirectToAuth(); return response; } - const retryHeaders = new Headers(init?.headers); - const newToken = getAuthToken(); - if (newToken) { - retryHeaders.set("Authorization", `Bearer ${newToken}`); - } else { - clearAuthTokens(); - } - - return fetch(input, { ...init, headers: retryHeaders }); + if (!getAuthToken()) clearAuthTokens(); + return retryWithCurrentToken(resolvedInput, init); } export function logout(): void { diff --git a/studio/frontend/src/features/auth/components/auth-form.tsx b/studio/frontend/src/features/auth/components/auth-form.tsx index d9190429bd..090a3081a4 100644 --- a/studio/frontend/src/features/auth/components/auth-form.tsx +++ b/studio/frontend/src/features/auth/components/auth-form.tsx @@ -1,6 +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 { apiUrl } from "@/lib/api-base"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; @@ -49,7 +50,7 @@ async function loginWithPassword( username: string, password: string, ): Promise { - const response = await fetch("/api/auth/login", { + const response = await fetch(apiUrl("/api/auth/login"), { method: "POST", headers: { "Content-Type": "application/json", @@ -96,7 +97,7 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { // (e.g. tokens from a previous install attempt). The server's // /api/auth/status is the source of truth for requires_password_change. try { - const response = await fetch("/api/auth/status"); + const response = await fetch(apiUrl("/api/auth/status")); if (!response.ok) throw new Error("Failed to load auth status."); const result = (await response.json()) as AuthStatusResponse; if (!canceled) { @@ -147,14 +148,15 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { }; }, [navigate]); - // Seed password from bootstrap credentials injected into HTML + // Seed password from bootstrap credentials injected into HTML by web CLI. useEffect(() => { - const bootstrap = window.__UNSLOTH_BOOTSTRAP__; - if (bootstrap) { - if (!isLoginMode && !password) { + function loadBootstrap() { + const bootstrap = window.__UNSLOTH_BOOTSTRAP__; + if (bootstrap && !isLoginMode && !password) { setPassword(bootstrap.password); } } + loadBootstrap(); }, []); const blockedByState = @@ -241,7 +243,7 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null { accessToken = bootstrapToken.access_token; } - const response = await fetch("/api/auth/change-password", { + const response = await fetch(apiUrl("/api/auth/change-password"), { method: "POST", headers: { "Content-Type": "application/json", diff --git a/studio/frontend/src/features/auth/index.ts b/studio/frontend/src/features/auth/index.ts index af629cfb9a..9cc1599195 100644 --- a/studio/frontend/src/features/auth/index.ts +++ b/studio/frontend/src/features/auth/index.ts @@ -15,3 +15,8 @@ export { resetOnboardingDone, setMustChangePassword, } from "./session"; +export { + clearTauriAuthFailure, + getTauriAuthFailure, + tauriAutoAuth, +} from "./tauri-auto-auth"; diff --git a/studio/frontend/src/features/auth/session.ts b/studio/frontend/src/features/auth/session.ts index 6012174077..49a2722bdf 100644 --- a/studio/frontend/src/features/auth/session.ts +++ b/studio/frontend/src/features/auth/session.ts @@ -2,6 +2,7 @@ // Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 import { usePlatformStore } from "@/config/env"; +import { isTauri } from "@/lib/api-base"; export const AUTH_TOKEN_KEY = "unsloth_auth_token"; export const AUTH_REFRESH_TOKEN_KEY = "unsloth_auth_refresh_token"; @@ -78,6 +79,7 @@ export function resetOnboardingDone(): void { } export function getPostAuthRoute(): PostAuthRoute { + if (isTauri) return "/chat"; if (mustChangePassword()) return "/change-password"; if (usePlatformStore.getState().isChatOnly()) return "/chat"; return "/chat"; diff --git a/studio/frontend/src/features/auth/tauri-auto-auth.ts b/studio/frontend/src/features/auth/tauri-auto-auth.ts new file mode 100644 index 0000000000..d67730d2d6 --- /dev/null +++ b/studio/frontend/src/features/auth/tauri-auto-auth.ts @@ -0,0 +1,95 @@ +// 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 { isTauri } from "@/lib/api-base"; +import { + hasAuthToken, + hasRefreshToken, + mustChangePassword, + storeAuthTokens, +} from "./session"; +import { refreshSession } from "./api"; + +type DesktopAuthResponse = { + access_token: string; + refresh_token: string; +}; + +// Concurrency guard: multiple route guards can call tauriAutoAuth simultaneously. +// Without this, the first-launch password-change could race with itself. +let pending: Promise | null = null; +let lastTauriAuthFailure: string | null = null; + +const TAURI_AUTH_FAILURE_FALLBACK = + "Desktop authentication failed. Update or repair the managed Studio install, then restart Studio."; +const BACKEND_NOT_READY_MESSAGE = "Backend is not ready"; + +function authFailureMessage(error: unknown): string { + if (typeof error === "string" && error) return error; + if (error instanceof Error && error.message) return error.message; + return TAURI_AUTH_FAILURE_FALLBACK; +} + +export function getTauriAuthFailure(): string | null { + return lastTauriAuthFailure; +} + +export function clearTauriAuthFailure(): void { + lastTauriAuthFailure = null; +} + +function setTauriAuthFailure(error: unknown): void { + lastTauriAuthFailure = authFailureMessage(error); + window.dispatchEvent( + new CustomEvent("tauri-auth-failed", { detail: lastTauriAuthFailure }), + ); +} + +function isBackendNotReady(error: unknown): boolean { + return authFailureMessage(error).includes(BACKEND_NOT_READY_MESSAGE); +} + +async function doTauriAutoAuth(): Promise { + // Desktop must handle password-change state internally in Rust. + if (hasAuthToken() && !mustChangePassword()) { + clearTauriAuthFailure(); + return true; + } + + // Try refreshing existing session + if (hasRefreshToken()) { + const refreshed = await refreshSession(); + if (refreshed && hasAuthToken() && !mustChangePassword()) { + clearTauriAuthFailure(); + return true; + } + } + + try { + const { invoke } = await import("@tauri-apps/api/core"); + const tokens = await invoke("desktop_auth"); + storeAuthTokens(tokens.access_token, tokens.refresh_token, false); + clearTauriAuthFailure(); + return true; + } catch (error) { + if (isBackendNotReady(error)) return false; + setTauriAuthFailure(error); + return false; + } +} + +/** + * Silently authenticate in Tauri desktop mode. + * + * Delegates bootstrap/password handling to Rust and only stores returned tokens. + * + * Returns true if authentication succeeded. + * Concurrent calls are coalesced into a single in-flight attempt. + */ +export function tauriAutoAuth(): Promise { + if (!isTauri) return Promise.resolve(false); + if (!pending) { + pending = doTauriAutoAuth().finally(() => { pending = null; }); + } + return pending; +} diff --git a/studio/frontend/src/features/settings/tabs/about-tab.tsx b/studio/frontend/src/features/settings/tabs/about-tab.tsx index 417922139c..bb4454aa43 100644 --- a/studio/frontend/src/features/settings/tabs/about-tab.tsx +++ b/studio/frontend/src/features/settings/tabs/about-tab.tsx @@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button"; import { ShutdownDialog } from "@/components/shutdown-dialog"; import { UpdateStudioInstructions } from "../components/update-studio-instructions"; import { usePlatformStore } from "@/config/env"; +import { apiUrl } from "@/lib/api-base"; import { removeTrainingUnloadGuard } from "@/features/training/hooks/use-training-unload-guard"; import { ArrowUpRight01Icon, @@ -28,7 +29,7 @@ export function AboutTab() { (async () => { try { - const res = await fetch("/api/health"); + const res = await fetch(apiUrl("/api/health")); if (!res.ok) return; const data = (await res.json()) as { version?: string }; if (!canceled && data.version) { diff --git a/studio/frontend/src/hooks/index.ts b/studio/frontend/src/hooks/index.ts index f889dc7445..cfbd8e5115 100644 --- a/studio/frontend/src/hooks/index.ts +++ b/studio/frontend/src/hooks/index.ts @@ -11,4 +11,5 @@ export { useHfDatasetSearch } from "./use-hf-dataset-search"; export { useHfDatasetSplits } from "./use-hf-dataset-splits"; export { useHfTokenValidation } from "./use-hf-token-validation"; export { useInfiniteScroll } from "./use-infinite-scroll"; +export { useTauriBackend } from "./use-tauri-backend"; export { useCollapseScrollLock } from "./use-collapse-scroll-lock"; diff --git a/studio/frontend/src/hooks/use-gpu-info.ts b/studio/frontend/src/hooks/use-gpu-info.ts index 3eb81c35e9..64caf06b50 100644 --- a/studio/frontend/src/hooks/use-gpu-info.ts +++ b/studio/frontend/src/hooks/use-gpu-info.ts @@ -1,6 +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 { apiUrl } from "@/lib/api-base"; import { useEffect, useState } from "react"; export interface GpuInfo { @@ -27,7 +28,7 @@ async function fetchGpuOnce(): Promise { fetchPromise = (async () => { try { - const res = await fetch("/api/system"); + const res = await fetch(apiUrl("/api/system")); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); const gpuData = data?.gpu; diff --git a/studio/frontend/src/hooks/use-tauri-backend.ts b/studio/frontend/src/hooks/use-tauri-backend.ts new file mode 100644 index 0000000000..ed697fe31e --- /dev/null +++ b/studio/frontend/src/hooks/use-tauri-backend.ts @@ -0,0 +1,487 @@ +// 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 { useEffect, useState, useCallback, useRef } from "react"; +import { isTauri, setApiBase } from "@/lib/api-base"; +import { + clearTauriAuthFailure, + getTauriAuthFailure, +} from "@/features/auth"; + +export type BackendStatus = + | "checking" + | "not-installed" + | "installing" + | "install-error" + | "needs-elevation" + | "repairing" + | "repair-error" + | "starting" + | "running" + | "stopped" + | "error"; + +type DesktopPreflightDisposition = + | "not_installed" + | "managed_ready" + | "managed_stale" + | "attached_ready"; + +interface DesktopPreflightResult { + disposition: DesktopPreflightDisposition; + reason: string | null; + port: number | null; + can_auto_repair: boolean; + managed_bin: string | null; +} + +export function useTauriBackend() { + const [status, setStatus] = useState("checking"); + const statusRef = useRef(status); + const [logs, setLogs] = useState([]); + const [error, setError] = useState(null); + // Guard against double startServer calls + const startingRef = useRef(false); + // Guard against React Strict Mode double-mount + const mountedRef = useRef(false); + // Track the discovered port from server-port event + const portRef = useRef(null); + const [currentStepIndex, setCurrentStepIndex] = useState(-1); + const [elevationPackages, setElevationPackages] = useState([]); + const [progressDetail, setProgressDetail] = useState(null); + // Track seen step names to deduplicate (Strict Mode, event replay, etc.) + const seenStepsRef = useRef(new Set()); + // True when we attached to a server we didn't spawn (can't stop it) + const [isExternalServer, setIsExternalServer] = useState(false); + const externalPollRef = useRef | null>(null); + const externalPollAbortedRef = useRef(false); + const authFailureRef = useRef(getTauriAuthFailure()); + const elevationResumeRef = useRef<"install" | "repair" | null>(null); + + function setBackendStatus(nextStatus: BackendStatus) { + if (authFailureRef.current) return; + statusRef.current = nextStatus; + setStatus(nextStatus); + } + + function setBackendError( + nextError: string, + nextStatus: BackendStatus = "error", + ) { + if (authFailureRef.current) return; + statusRef.current = nextStatus; + setStatus(nextStatus); + setError(nextError); + } + + function clearBackendError() { + if (authFailureRef.current) return; + setError(null); + } + + function setRunningStatus() { + setBackendStatus("running"); + } + + function setAuthFailure(detail: string) { + authFailureRef.current = detail; + statusRef.current = "error"; + setStatus("error"); + setError(detail); + } + + function clearAuthFailure() { + authFailureRef.current = null; + clearTauriAuthFailure(); + } + + function stopExternalServerPoll() { + externalPollAbortedRef.current = true; + if (externalPollRef.current) { + clearInterval(externalPollRef.current); + externalPollRef.current = null; + } + } + + function startExternalServerPoll(port: number) { + stopExternalServerPoll(); + externalPollAbortedRef.current = false; + let failures = 0; + externalPollRef.current = setInterval(async () => { + if (externalPollAbortedRef.current) return; + try { + const { invoke } = await import("@tauri-apps/api/core"); + const healthy = await invoke("check_health", { port }); + if (externalPollAbortedRef.current) return; + if (healthy) { + failures = 0; + } else { + failures++; + } + } catch { + if (externalPollAbortedRef.current) return; + failures++; + } + if (failures >= 3) { + stopExternalServerPoll(); + setIsExternalServer(false); + setBackendError("External server is no longer responding"); + } + }, 15_000); + } + + // Keep ref in sync for event listener closures + useEffect(() => { + statusRef.current = status; + }, [status]); + + async function checkInstallAndStart() { + try { + const { invoke } = await import("@tauri-apps/api/core"); + + const preflight = await invoke("desktop_preflight"); + switch (preflight.disposition) { + case "attached_ready": { + if (!preflight.port) { + setBackendError("Desktop preflight found a backend without a port."); + return; + } + setApiBase(preflight.port); + portRef.current = preflight.port; + setIsExternalServer(true); + setRunningStatus(); + startExternalServerPoll(preflight.port); + return; + } + case "managed_ready": + setIsExternalServer(false); + stopExternalServerPoll(); + setBackendStatus("starting"); + await startManagedServer(); + return; + case "managed_stale": + setIsExternalServer(false); + stopExternalServerPoll(); + if (preflight.can_auto_repair) { + await startRepair(); + } else { + setBackendError( + "Managed Studio install is too old. Run `unsloth studio update`.", + ); + } + return; + case "not_installed": + setBackendStatus("not-installed"); + return; + } + } catch (e) { + setBackendError(String(e)); + } + } + + async function startManagedServer() { + // Prevent double-start race condition + if (startingRef.current) return; + startingRef.current = true; + + try { + const { invoke } = await import("@tauri-apps/api/core"); + // backend/run.py keeps the existing 8888-8908 fallback via + // server-port/TAURI_PORT. + await invoke("start_managed_server", { port: 8888 }); + + // Wait for the owned backend's server-port event. Do not attach to an + // external backend if the managed start does not report a port. + for (let i = 0; i < 120; i++) { + if (portRef.current) { + const healthy = await invoke("check_health", { + port: portRef.current, + }); + if (healthy) { + setApiBase(portRef.current); + setRunningStatus(); + startingRef.current = false; + return; + } + } + await new Promise((r) => setTimeout(r, 500)); + } + const message = !portRef.current + ? "Managed server started without reporting a port. Check the logs for details." + : "Server started but is not responding. Check the logs for details."; + setBackendError(message); + } catch (e) { + const msg = String(e); + if (msg.includes("already running")) { + startingRef.current = false; + setBackendError( + "Managed server is already running but did not report a port. Restart Studio and try again.", + ); + return; + } + setBackendError(msg); + } + startingRef.current = false; + } + + async function startRepair() { + elevationResumeRef.current = null; + setCurrentStepIndex(-1); + setProgressDetail(null); + seenStepsRef.current.clear(); + startingRef.current = false; + portRef.current = null; + setIsExternalServer(false); + stopExternalServerPoll(); + setLogs([]); + clearBackendError(); + setBackendStatus("repairing"); + + const { invoke } = await import("@tauri-apps/api/core"); + try { + await invoke("start_managed_repair"); + + setBackendStatus("starting"); + elevationResumeRef.current = null; + await startManagedServer(); + } catch (e) { + const msg = String(e); + if (msg.includes("NEEDS_ELEVATION")) return; + setBackendError(msg, "repair-error"); + } + } + + async function startServer() { + setBackendStatus("starting"); + await startManagedServer(); + } + + async function stopServer() { + if (isExternalServer) { + // We attached to a server we didn't spawn — can't kill it, + // just disconnect the UI. + startingRef.current = false; + setIsExternalServer(false); + stopExternalServerPoll(); + setBackendStatus("stopped"); + return; + } + const { invoke } = await import("@tauri-apps/api/core"); + await invoke("stop_server"); + startingRef.current = false; + setBackendStatus("stopped"); + } + + async function startInstall() { + elevationResumeRef.current = null; + setCurrentStepIndex(-1); + setProgressDetail(null); + seenStepsRef.current.clear(); + setBackendStatus("installing"); + setLogs([]); + clearBackendError(); + const { invoke } = await import("@tauri-apps/api/core"); + try { + await invoke("start_install"); + // Install completed — this is the ONLY path that starts the server + // after install. The install-complete event listener does NOT call + // startServer() to avoid a double-start race condition. + setBackendStatus("starting"); + elevationResumeRef.current = null; + await startServer(); + } catch (e) { + const msg = String(e); + // NEEDS_ELEVATION is not a real error — the Rust side also emits + // install-needs-elevation which sets needs-elevation status. + // Don't race with it by setting install-error here. + if (msg.includes("NEEDS_ELEVATION")) return; + setBackendError(msg, "install-error"); + } + } + + const retry = useCallback(() => { + clearAuthFailure(); + setError(null); + setLogs([]); + startingRef.current = false; + portRef.current = null; + setCurrentStepIndex(-1); + setProgressDetail(null); + setElevationPackages([]); + elevationResumeRef.current = null; + setIsExternalServer(false); + stopExternalServerPoll(); + seenStepsRef.current.clear(); + checkInstallAndStart(); + }, []); + + const retryInstall = useCallback(() => { + const resume = elevationResumeRef.current; + elevationResumeRef.current = null; + clearBackendError(); + setLogs([]); + setElevationPackages([]); + if (resume === "repair") { + setBackendError("Repair canceled before system packages were installed.", "repair-error"); + return; + } + setBackendStatus("not-installed"); + }, []); + + const approveElevation = useCallback(async () => { + const resume = elevationResumeRef.current ?? "install"; + try { + const { invoke } = await import("@tauri-apps/api/core"); + await invoke("install_system_packages", { packages: elevationPackages }); + // Packages installed successfully, resume the flow that requested them. + setCurrentStepIndex(-1); + setProgressDetail(null); + elevationResumeRef.current = null; + if (resume === "repair") { + await startRepair(); + } else { + await startInstall(); + } + } catch (e) { + setBackendError(String(e), resume === "repair" ? "repair-error" : "install-error"); + } + }, [elevationPackages]); + + // Initial check on mount (guarded against Strict Mode double-mount) + useEffect(() => { + if (mountedRef.current) return; + mountedRef.current = true; + + if (!isTauri) { + setRunningStatus(); + return; + } + checkInstallAndStart(); + }, []); + + // Listen for Tauri events + useEffect(() => { + if (!isTauri) return; + const cleanup: (() => void)[] = []; + let disposed = false; + + import("@tauri-apps/api/event").then(({ listen }) => { + function register( + event: string, + handler: Parameters>[1], + ) { + listen(event, handler).then((unlisten) => { + if (disposed) { + unlisten(); + } else { + cleanup.push(unlisten); + } + }); + } + + register("install-progress", (e) => { + setLogs((prev) => [...prev.slice(-499), e.payload]); + }); + + // install-complete is informational only — does NOT trigger startServer. + // The invoke("start_install") success path handles that to avoid races. + register("install-complete", () => { + setCurrentStepIndex(999); // all steps done + }); + + register("install-step", (e) => { + const stepName = e.payload; + if (seenStepsRef.current.has(stepName)) return; // deduplicate + seenStepsRef.current.add(stepName); + setCurrentStepIndex((prev) => prev + 1); + setProgressDetail(null); + }); + + register("install-needs-elevation", (e) => { + elevationResumeRef.current = "install"; + setElevationPackages(e.payload); + setBackendStatus("needs-elevation"); + }); + + register("install-progress-detail", (e) => { + setProgressDetail(e.payload); + }); + + register("install-failed", (e) => { + setBackendError(e.payload, "install-error"); + }); + + register("repair-progress", (e) => { + setLogs((prev) => [...prev.slice(-499), e.payload]); + }); + + register("repair-needs-elevation", (e) => { + elevationResumeRef.current = "repair"; + setElevationPackages(e.payload); + setBackendStatus("needs-elevation"); + }); + + register("repair-complete", () => { + if (statusRef.current !== "repairing") return; + setProgressDetail("Repair complete"); + }); + + register("repair-failed", (e) => { + if (statusRef.current !== "repairing") return; + setBackendError(e.payload, "repair-error"); + }); + + register("server-port", (e) => { + portRef.current = e.payload; + setApiBase(e.payload); + }); + + register("server-crashed", () => { + startingRef.current = false; + setBackendError("Server stopped unexpectedly"); + }); + + register("server-log", (e) => { + setLogs((prev) => [...prev.slice(-499), e.payload]); + }); + + register("tray-toggle-server", () => { + if (statusRef.current === "running") { + stopServer(); + } else if ( + statusRef.current === "stopped" || + statusRef.current === "error" + ) { + retry(); + } + }); + }); + + const onAuthFailed = (event: Event) => { + const detail = + event instanceof CustomEvent && typeof event.detail === "string" + ? event.detail + : "Desktop authentication failed. Update or repair the managed Studio install, then restart Studio."; + setAuthFailure(detail); + }; + window.addEventListener("tauri-auth-failed", onAuthFailed); + const authFailure = getTauriAuthFailure(); + if (authFailure) setAuthFailure(authFailure); + cleanup.push(() => + window.removeEventListener("tauri-auth-failed", onAuthFailed), + ); + + return () => { + disposed = true; + cleanup.forEach((fn) => fn()); + stopExternalServerPoll(); + }; + }, []); + + return { + status, logs, error, isExternalServer, + currentStepIndex, progressDetail, elevationPackages, + startServer, stopServer, startInstall, + retry, retryInstall, approveElevation, + }; +} diff --git a/studio/frontend/src/hooks/use-tauri-update.ts b/studio/frontend/src/hooks/use-tauri-update.ts new file mode 100644 index 0000000000..5863ef7aa5 --- /dev/null +++ b/studio/frontend/src/hooks/use-tauri-update.ts @@ -0,0 +1,219 @@ +// 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 { useEffect, useRef, useState } from "react"; +import { isTauri } from "@/lib/api-base"; +import { toast } from "sonner"; + +export type UpdateStatus = + | "idle" + | "checking" + | "available" + | "updating-backend" + | "downloading" + | "installing" + | "error"; + +export interface UpdateInfo { + version: string; + currentVersion: string; + body?: string; + date?: string; +} + +export function useTauriUpdate(isExternalServer = false) { + const [status, setStatus] = useState("idle"); + const [info, setInfo] = useState(null); + const [progress, setProgress] = useState(0); + const [logs, setLogs] = useState([]); + const [dismissed, setDismissed] = useState(false); + const [error, setError] = useState(null); + const updateRef = useRef + > | null>(null); + const checkedRef = useRef(false); + const updatingRef = useRef(false); + + useEffect(() => { + if (!isTauri || checkedRef.current) return; + checkedRef.current = true; + + async function checkForUpdate() { + setStatus("checking"); + try { + const { check } = await import("@tauri-apps/plugin-updater"); + const update = await check(); + if (update) { + updateRef.current = update; + setInfo({ + version: update.version, + currentVersion: update.currentVersion, + body: update.body, + date: update.date, + }); + setStatus("available"); + } else { + setStatus("idle"); + } + } catch (e) { + console.error("Update check failed:", e); + setStatus("idle"); + } + } + + const timer = setTimeout(checkForUpdate, 5000); + return () => clearTimeout(timer); + }, []); + + async function installUpdate() { + const update = updateRef.current; + if (!update || updatingRef.current) return; + updatingRef.current = true; + + const cleanups: (() => void)[] = []; + let phase: "backend" | "shell" = "backend"; + + try { + // ── Step 1: Backend update ── + setStatus("updating-backend"); + setLogs([]); + setError(null); + setDismissed(false); + + const { listen } = await import("@tauri-apps/api/event"); + const { invoke } = await import("@tauri-apps/api/core"); + + // Listen for backend update progress + const unlistenProgress = await listen( + "update-progress", + (e) => { + setLogs((prev) => [...prev.slice(-499), e.payload]); + }, + ); + cleanups.push(unlistenProgress); + + // Wait for complete or failed + const backendResult = await new Promise<"complete" | string>( + (resolve) => { + listen("update-complete", () => resolve("complete")).then( + (u) => cleanups.push(u), + ); + listen("update-failed", (e) => + resolve(e.payload), + ).then((u) => cleanups.push(u)); + + invoke("start_backend_update").catch((e) => resolve(String(e))); + }, + ); + + if (backendResult !== "complete") { + setError(backendResult); + setStatus("error"); + updatingRef.current = false; + cleanup(cleanups); + return; + } + + // ── Step 2: Shell update ── + phase = "shell"; + setStatus("downloading"); + setProgress(0); + + let downloaded = 0; + let contentLength = 0; + await update.downloadAndInstall((event) => { + switch (event.event) { + case "Started": + contentLength = event.data.contentLength ?? 0; + break; + case "Progress": + downloaded += event.data.chunkLength; + if (contentLength > 0) { + setProgress(Math.round((downloaded / contentLength) * 100)); + } + break; + case "Finished": + setStatus("installing"); + break; + } + }); + + // ── Step 3: Relaunch ── + const { relaunch } = await import("@tauri-apps/plugin-process"); + await relaunch(); + } catch (e) { + console.error("Update failed:", e); + const msg = String(e); + + // Shell update failed — restart backend on updated code + if (phase === "shell") { + try { + const { invoke } = await import("@tauri-apps/api/core"); + await invoke("start_server", { port: 8888 }); + toast.error("App update failed", { + description: + "Backend was updated. The app update will be retried on next launch.", + }); + setStatus("idle"); + setDismissed(true); + } catch { + setError(msg); + setStatus("error"); + } + } else { + setError(msg); + setStatus("error"); + } + } finally { + updatingRef.current = false; + cleanup(cleanups); + } + } + + async function retryUpdate() { + updatingRef.current = false; + await installUpdate(); + } + + async function skipAndRestart() { + try { + const { invoke } = await import("@tauri-apps/api/core"); + await invoke("start_server", { port: 8888 }); + setStatus("idle"); + setError(null); + setLogs([]); + setDismissed(true); + } catch (e) { + setError(String(e)); + setStatus("error"); + } + } + + function dismiss() { + setDismissed(true); + } + + return { + status, + info, + progress, + logs, + dismissed, + error, + isExternalServer, + installUpdate, + retryUpdate, + skipAndRestart, + dismiss, + }; +} + +function cleanup(fns: (() => void)[]) { + for (const fn of fns) { + try { + fn(); + } catch { + // ignore + } + } +} diff --git a/studio/frontend/src/lib/api-base.ts b/studio/frontend/src/lib/api-base.ts new file mode 100644 index 0000000000..664b9e58bd --- /dev/null +++ b/studio/frontend/src/lib/api-base.ts @@ -0,0 +1,24 @@ +// Central API base URL for Tauri vs browser mode +let apiBase = '' + +const isTauri = typeof window !== 'undefined' && '__TAURI__' in window +const isViteDev = import.meta.env.DEV + +if (isTauri && !isViteDev) { + apiBase = 'http://127.0.0.1:8888' +} + +export function setApiBase(port: number) { + apiBase = `http://127.0.0.1:${port}` +} + +export function getApiBase(): string { + return apiBase +} + +export function apiUrl(path: string): string { + if (path.startsWith('http')) return path + return `${apiBase}${path}` +} + +export { isTauri } diff --git a/studio/frontend/src/lib/open-link.ts b/studio/frontend/src/lib/open-link.ts new file mode 100644 index 0000000000..b7e6a0fa6e --- /dev/null +++ b/studio/frontend/src/lib/open-link.ts @@ -0,0 +1,31 @@ +import { isTauri } from "@/lib/api-base"; + +/** + * Open a URL in the system browser (Tauri) or new tab (web). + * Handles anchor links and mailto: natively without the opener plugin. + * Returns true when the caller should preventDefault; false when the + * browser's native navigation should proceed (relative URLs, empty, etc.). + */ +export function openLink(url: string): boolean { + if (!url) return false; + + // Anchor links — scroll within the page, don't open externally + if (url.startsWith("#")) { + window.location.hash = url; + return true; + } + + // Relative URLs — let the browser / router handle them natively + if (!url.includes("://") && !url.startsWith("mailto:")) { + return false; + } + + if (isTauri) { + import("@tauri-apps/plugin-opener").then(({ openUrl }) => { + openUrl(url).catch(console.error); + }); + } else { + window.open(url, "_blank", "noopener,noreferrer"); + } + return true; +} diff --git a/studio/install_llama_prebuilt.py b/studio/install_llama_prebuilt.py index 1ad1ccc851..23e6b7c0e4 100755 --- a/studio/install_llama_prebuilt.py +++ b/studio/install_llama_prebuilt.py @@ -46,6 +46,28 @@ EXIT_ERROR = 1 EXIT_BUSY = 3 +def windows_hidden_subprocess_kwargs() -> dict[str, object]: + """Return Windows-only subprocess kwargs that suppress console windows.""" + if sys.platform != "win32": + return {} + + kwargs: dict[str, object] = {} + create_no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0) + if create_no_window: + kwargs["creationflags"] = create_no_window + + startupinfo_factory = getattr(subprocess, "STARTUPINFO", None) + startf_use_showwindow = getattr(subprocess, "STARTF_USESHOWWINDOW", 0) + sw_hide = getattr(subprocess, "SW_HIDE", 0) + if startupinfo_factory is not None and startf_use_showwindow: + startupinfo = startupinfo_factory() + startupinfo.dwFlags |= startf_use_showwindow + startupinfo.wShowWindow = sw_hide + kwargs["startupinfo"] = startupinfo + + return kwargs + + def env_int(name: str, default: int, *, minimum: int | None = None) -> int: raw = os.environ.get(name) if raw is None: @@ -2469,6 +2491,7 @@ def run_capture( text = True, timeout = timeout, env = env, + **windows_hidden_subprocess_kwargs(), ) if check and result.returncode != 0: raise subprocess.CalledProcessError( @@ -4357,6 +4380,7 @@ def validate_quantize( text = True, timeout = 120, env = binary_env(quantize_path, install_dir, host, runtime_line = runtime_line), + **windows_hidden_subprocess_kwargs(), ) if ( result.returncode != 0 @@ -4444,6 +4468,7 @@ def validate_server( env = binary_env( server_path, install_dir, host, runtime_line = runtime_line ), + **windows_hidden_subprocess_kwargs(), ) deadline = time.time() + 20 startup_started = time.time() diff --git a/studio/install_python_stack.py b/studio/install_python_stack.py index 2b9fd084d4..b56b737cfa 100644 --- a/studio/install_python_stack.py +++ b/studio/install_python_stack.py @@ -359,6 +359,28 @@ def _ensure_rocm_torch() -> None: ) +def _windows_hidden_subprocess_kwargs() -> dict[str, object]: + """Return Windows-only subprocess kwargs that suppress console windows.""" + if not IS_WINDOWS: + return {} + + kwargs: dict[str, object] = {} + create_no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0) + if create_no_window: + kwargs["creationflags"] = create_no_window + + startupinfo_factory = getattr(subprocess, "STARTUPINFO", None) + startf_use_showwindow = getattr(subprocess, "STARTF_USESHOWWINDOW", 0) + sw_hide = getattr(subprocess, "SW_HIDE", 0) + if startupinfo_factory is not None and startf_use_showwindow: + startupinfo = startupinfo_factory() + startupinfo.dwFlags |= startf_use_showwindow + startupinfo.wShowWindow = sw_hide + kwargs["startupinfo"] = startupinfo + + return kwargs + + def _infer_no_torch() -> bool: """Determine whether to run in no-torch (GGUF-only) mode. @@ -412,9 +434,11 @@ _UNICODE_TO_ASCII: dict[str, str] = { def _safe_print(*args: object, **kwargs: object) -> None: - """Drop-in print() replacement that survives non-UTF-8 consoles.""" + """Drop-in print() replacement that survives non-UTF-8 consoles and detached stdout.""" try: print(*args, **kwargs) + except OSError: + return except UnicodeEncodeError: # Stringify, then swap emoji for ASCII equivalents text = " ".join(str(a) for a in args) @@ -495,7 +519,7 @@ def _step(label: str, value: str, color_fn = None) -> None: if color_fn is None: color_fn = _green padded = label[:_COL] - print(f" {_dim(padded)}{' ' * (_COL - len(padded))}{color_fn(value)}") + _safe_print(f" {_dim(padded)}{' ' * (_COL - len(padded))}{color_fn(value)}") def _progress(label: str) -> None: @@ -509,10 +533,13 @@ def _progress(label: str) -> None: bar = "=" * filled + "-" * (width - filled) pad = " " * (_COL - len(_LABEL)) end = "\n" if _STEP >= _TOTAL else "" - sys.stdout.write( - f"\r {_dim(_LABEL)}{pad}[{bar}] {_STEP:2}/{_TOTAL} {label:<20}{end}" - ) - sys.stdout.flush() + try: + sys.stdout.write( + f"\r {_dim(_LABEL)}{pad}[{bar}] {_STEP:2}/{_TOTAL} {label:<20}{end}" + ) + sys.stdout.flush() + except OSError: + pass def run( @@ -525,6 +552,7 @@ def run( cmd, stdout = subprocess.PIPE if quiet else None, stderr = subprocess.STDOUT if quiet else None, + **_windows_hidden_subprocess_kwargs(), ) if result.returncode != 0: _step("error", f"{label} failed (exit code {result.returncode})", _red) @@ -627,6 +655,7 @@ def _bootstrap_uv() -> bool: ["uv", "pip", "install", "--dry-run", "--python", sys.executable, "pip"], stdout = subprocess.PIPE, stderr = subprocess.STDOUT, + **_windows_hidden_subprocess_kwargs(), ) if probe.returncode != 0: # Retry with --system (some envs need it when uv can't find a venv) @@ -634,6 +663,7 @@ def _bootstrap_uv() -> bool: ["uv", "pip", "install", "--dry-run", "--system", "pip"], stdout = subprocess.PIPE, stderr = subprocess.STDOUT, + **_windows_hidden_subprocess_kwargs(), ) if probe_sys.returncode != 0: return False # uv is broken, fall back to pip @@ -773,6 +803,7 @@ def pip_install( uv_cmd, stdout = subprocess.PIPE, stderr = subprocess.STDOUT, + **_windows_hidden_subprocess_kwargs(), ) if result.returncode == 0: return @@ -798,6 +829,7 @@ def patch_package_file(package_name: str, relative_path: str, url: str) -> None: [sys.executable, "-m", "pip", "show", package_name], capture_output = True, text = True, + **_windows_hidden_subprocess_kwargs(), ) if result.returncode != 0: _step(_LABEL, f"package {package_name} not found, skipping patch", _red) @@ -868,6 +900,7 @@ def install_python_stack() -> int: [sys.executable, "-m", "pip", "--version"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL, + **_windows_hidden_subprocess_kwargs(), ).returncode == 0 ) @@ -1140,6 +1173,7 @@ def install_python_stack() -> int: [sys.executable, "-m", "pip", "check"], stdout = subprocess.DEVNULL, stderr = subprocess.DEVNULL, + **_windows_hidden_subprocess_kwargs(), ) _step(_LABEL, "installed") diff --git a/studio/setup.ps1 b/studio/setup.ps1 index 218be7e045..24ef3ef1eb 100644 --- a/studio/setup.ps1 +++ b/studio/setup.ps1 @@ -1077,10 +1077,13 @@ if (-not $CudaArch) { } # ============================================ -# 1f. Node.js / npm (skip if pip-installed -- only needed for frontend build) +# 1f. Node.js / npm (skip if pip-installed or Tauri -- only needed for frontend build) # ============================================ +$SkipFrontend = ($env:SKIP_STUDIO_FRONTEND -eq "1") if ($IsPipInstall) { step "frontend" "bundled (pip install)" +} elseif ($SkipFrontend) { + step "frontend" "bundled (Tauri)" } else { # setup.sh installs Node LTS (v22) via nvm. We enforce the same range here: # Vite 8 requires Node ^20.19.0 || >=22.12.0, npm >= 11. @@ -1207,6 +1210,9 @@ $NeedFrontendBuild = $true if ($IsPipInstall) { $NeedFrontendBuild = $false step "frontend" "bundled (pip install)" +} elseif ($SkipFrontend) { + $NeedFrontendBuild = $false + step "frontend" "bundled (Tauri)" } elseif (Test-Path $DistDir) { $DistTime = (Get-Item $DistDir).LastWriteTime $NewerFile = $null @@ -1539,7 +1545,7 @@ if (Get-Command uv -ErrorAction SilentlyContinue) { } else { substep "installing uv package manager..." try { - Invoke-SetupCommand { powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" } | Out-Null + Invoke-SetupCommand { Invoke-Expression (Invoke-RestMethod -Uri "https://astral.sh/uv/install.ps1") } | Out-Null Refresh-Environment # Re-activate venv since Refresh-Environment rebuilds PATH from # registry and drops the venv's Scripts directory diff --git a/studio/setup.sh b/studio/setup.sh index c2044b5141..142d253554 100755 --- a/studio/setup.sh +++ b/studio/setup.sh @@ -183,7 +183,13 @@ if [[ "$keynames" == *$'\nCOLAB_'* ]]; then fi if [ "$_LLAMA_ONLY" != "1" ]; then -# ── Frontend ── +# ── Detect whether frontend needs building ── +# Skip if SKIP_STUDIO_FRONTEND=1 (Tauri desktop app bundles its own frontend), +# or if dist/ exists AND no tracked input is newer than dist/. +if [ "${SKIP_STUDIO_FRONTEND:-0}" = "1" ]; then + _NEED_FRONTEND_BUILD=false + step "frontend" "bundled (Tauri)" +else _NEED_FRONTEND_BUILD=true if [ -d "$SCRIPT_DIR/frontend/dist" ]; then _changed=$(find "$SCRIPT_DIR/frontend" -maxdepth 1 -type f \ @@ -195,6 +201,7 @@ if [ -d "$SCRIPT_DIR/frontend/dist" ]; then fi [ -z "$_changed" ] && _NEED_FRONTEND_BUILD=false fi +fi # end SKIP_STUDIO_FRONTEND guard if [ "$_NEED_FRONTEND_BUILD" = false ]; then step "frontend" "up to date" @@ -484,9 +491,9 @@ _PKG_NAME="${STUDIO_PACKAGE_NAME:-unsloth}" if [ "$_SKIP_VERSION_CHECK" != true ] && [ "${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 "") +import sys; from importlib.metadata import version +print(version(sys.argv[1])) +" "$_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 \ diff --git a/studio/src-tauri/Cargo.lock b/studio/src-tauri/Cargo.lock new file mode 100644 index 0000000000..62d520437c --- /dev/null +++ b/studio/src-tauri/Cargo.lock @@ -0,0 +1,6320 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.0", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.29.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93d03419cb5950ccfd3daf3ff1c7a36ace64609a1a8746d493df1ca0afde0fa" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "matches", + "phf 0.10.1", + "proc-macro2", + "quote", + "smallvec", + "syn 1.0.109", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ctor" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser 0.36.0", + "foldhash 0.2.0", + "html5ever 0.38.0", + "precomputed-hash", + "selectors 0.36.1", + "tendril 0.5.0", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "elevated-command" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54c410eccdcc5b759704fdb6a792afe6b01ab8a062e2c003ff2567e2697a94aa" +dependencies = [ + "anyhow", + "base64 0.21.7", + "libc", + "log", + "winapi", + "windows 0.52.0", +] + +[[package]] +name = "embed-resource" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47ec73ddcf6b7f23173d5c3c5a32b5507dc0a734de7730aa14abc5d5e296bb5f" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 0.9.12+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fix-path-env" +version = "0.0.0" +source = "git+https://github.com/tauri-apps/fix-path-env-rs#c4c45d503ea115a839aae718d02f79e7c7f0f673" +dependencies = [ + "home", + "strip-ansi-escapes", + "thiserror 1.0.69", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "rand_core 0.10.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "html5ever" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b7410cae13cbc75623c98ac4cbfd1f0bedddf3227afc24f370cf0f50a44a11c" +dependencies = [ + "log", + "mac", + "markup5ever 0.14.1", + "match_token", +] + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever 0.38.0", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "kuchikiki" +version = "0.8.8-speedreader" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" +dependencies = [ + "cssparser 0.29.6", + "html5ever 0.29.1", + "indexmap 2.13.0", + "selectors 0.24.0", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "bitflags 2.11.0", + "libc", + "plain", + "redox_syscall 0.7.3", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "markup5ever" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a7213d12e1864c0f002f52c2923d4556935a43dec5e71355c2760e0f6e7a18" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache 0.8.9", + "string_cache_codegen 0.5.4", + "tendril 0.4.3", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril 0.5.0", + "web_atoms", +] + +[[package]] +name = "match_token" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minisign-verify" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "muda" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c1738382f66ed56b3b9c8119e794a2e23148ac8ea214eda86622d4cb9d415a" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-osa-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-app-kit", + "objc2-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.11.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "osakit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" +dependencies = [ + "objc2", + "objc2-foundation", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared 0.8.0", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +dependencies = [ + "phf_generator 0.8.0", + "phf_shared 0.8.0", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +dependencies = [ + "phf_shared 0.8.0", + "rand 0.7.3", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared 0.10.0", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator 0.10.0", + "phf_shared 0.10.0", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher 0.3.11", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher 1.0.2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64 0.22.1", + "indexmap 2.13.0", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.5+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "process-wrap" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e842efad9119158434d193c6682e2ebee4b44d6ad801d7b349623b3f57cdf55" +dependencies = [ + "indexmap 2.13.0", + "nix", + "tracing", + "windows 0.62.2", +] + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", + "rand_pcg", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.0", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c37578180969d00692904465fb7f6b3d50b9a2b952b87c23d0e2e5cb5013416" +dependencies = [ + "bitflags 1.3.2", + "cssparser 0.29.6", + "derive_more 0.99.20", + "fxhash", + "log", + "phf 0.8.0", + "phf_codegen 0.8.0", + "precomputed-hash", + "servo_arc 0.2.0", + "smallvec", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.11.0", + "cssparser 0.36.0", + "derive_more 2.1.1", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen 0.13.1", + "precomputed-hash", + "rustc-hash", + "servo_arc 0.4.3", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5414fad8e6907dbdd5bc441a50ae8d6e26151a03b1de04d89a5576de61d01f" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.13.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "servo_arc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52aa42f8fdf0fed91e5ce7f23d8138441002fa31dca008acf47e6fd4721f741" +dependencies = [ + "nodrop", + "stable_deref_trait", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall 0.5.18", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.11.3", + "precomputed-hash", + "serde", +] + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.11.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.34.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9103edf55f2da3c82aea4c7fab7c4241032bfeea0e71fa557d98e00e7ce7cc20" +dependencies = [ + "bitflags 2.11.0", + "block2", + "core-foundation 0.10.1", + "core-graphics", + "crossbeam-channel", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "parking_lot", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest 0.13.2", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows 0.61.3", +] + +[[package]] +name = "tauri-build" +version = "2.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.117", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "toml 0.9.12+spec-1.1.0", + "walkdir", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows 0.61.3", + "zbus", +] + +[[package]] +name = "tauri-plugin-process" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a" +dependencies = [ + "tauri", + "tauri-plugin", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc61e4822b8f74d68278e09161d3e3fdd1b14b9eb781e24edccaabf10c420e8c" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + +[[package]] +name = "tauri-plugin-updater" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af" +dependencies = [ + "base64 0.22.1", + "dirs", + "flate2", + "futures-util", + "http", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest 0.13.2", + "rustls", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.18", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip", +] + +[[package]] +name = "tauri-runtime" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows 0.61.3", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dunce", + "glob", + "html5ever 0.29.1", + "http", + "infer", + "json-patch", + "kuchikiki", + "log", + "memchr", + "phf 0.11.3", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" +dependencies = [ + "dunce", + "embed-resource", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.0.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b320e741db58cac564e26c607d3cc1fdc4a88fd36c879568c07856ed83ff3e9" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.13.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca1a40644a28bce036923f6a431df0b34236949d111cc07cb6dca830c9ef2e1" +dependencies = [ + "indexmap 2.13.0", + "toml_datetime 1.0.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.0", +] + +[[package]] +name = "toml_parser" +version = "1.0.10+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df25b4befd31c4816df190124375d5a20c6b6921e2cad937316de3fccd63420" +dependencies = [ + "winnow 1.0.0", +] + +[[package]] +name = "toml_writer" +version = "1.0.7+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17aaa1c6e3dc22b1da4b6bba97d066e354c7945cac2f7852d4e4e7ca7a6b56d" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png", + "serde", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsloth-studio" +version = "2026.4.7" +dependencies = [ + "dirs", + "elevated-command", + "fix-path-env", + "libc", + "log", + "open", + "process-wrap", + "rand 0.10.0", + "regex", + "reqwest 0.12.28", + "serde", + "serde_json", + "simplelog", + "tauri", + "tauri-build", + "tauri-plugin-opener", + "tauri-plugin-process", + "tauri-plugin-single-instance", + "tauri-plugin-updater", + "tokio", + "windows 0.62.2", + "windows-sys 0.59.0", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a9779e9f04d2ac1ce317aee707aa2f6b773afba7b931222bff6983843b1576" +dependencies = [ + "phf 0.13.1", + "phf_codegen 0.13.1", + "string_cache 0.9.0", + "string_cache_codegen 0.6.1", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows 0.61.3", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wry" +version = "0.54.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a8135d8676225e5744de000d4dff5a082501bf7db6a1c1495034f8c314edbc" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.15", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow 0.7.15", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.13.0", + "memchr", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.15", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 0.7.15", +] diff --git a/studio/src-tauri/Cargo.toml b/studio/src-tauri/Cargo.toml new file mode 100644 index 0000000000..ccb07cb9e2 --- /dev/null +++ b/studio/src-tauri/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "unsloth-studio" +version = "2026.4.7" +description = "Unsloth Studio Desktop App" +authors = ["Unsloth AI"] +edition = "2021" + +[dependencies] +tauri = { version = "2", features = ["tray-icon"] } +tauri-plugin-single-instance = "2" +tauri-plugin-process = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +reqwest = { version = "0.12", features = ["json"] } +tokio = { version = "1", features = ["full"] } +log = "0.4" +simplelog = "0.12" +dirs = "6" +regex = "1" +open = "5" +process-wrap = { version = "9", features = ["std"] } +fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" } +tauri-plugin-opener = "2.5.3" +tauri-plugin-updater = "2" +rand = "0.10.0" + +[target.'cfg(unix)'.dependencies] +libc = "0.2" + +[target.'cfg(target_os = "linux")'.dependencies] +elevated-command = "1.1.2" + +[target.'cfg(windows)'.dependencies] +windows = { version = "0.62.2", features = ["Win32_System_Threading"] } +windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Security", "Win32_System_Console", "Win32_System_JobObjects", "Win32_System_Threading"] } + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[features] +default = ["custom-protocol"] +custom-protocol = ["tauri/custom-protocol"] diff --git a/studio/src-tauri/Entitlements.plist b/studio/src-tauri/Entitlements.plist new file mode 100644 index 0000000000..10e6df3427 --- /dev/null +++ b/studio/src-tauri/Entitlements.plist @@ -0,0 +1,15 @@ + + + + + + com.apple.security.cs.allow-unsigned-executable-memory + + + com.apple.security.network.client + + + com.apple.security.cs.disable-library-validation + + + diff --git a/studio/src-tauri/build.rs b/studio/src-tauri/build.rs new file mode 100644 index 0000000000..d860e1e6a7 --- /dev/null +++ b/studio/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/studio/src-tauri/capabilities/default.json b/studio/src-tauri/capabilities/default.json new file mode 100644 index 0000000000..7b6033c721 --- /dev/null +++ b/studio/src-tauri/capabilities/default.json @@ -0,0 +1,24 @@ +{ + "identifier": "default", + "description": "Default capabilities for Unsloth Studio", + "windows": ["main"], + "permissions": [ + "core:default", + "core:window:default", + "core:window:allow-show", + "core:window:allow-hide", + "core:window:allow-set-focus", + "core:window:allow-set-size", + "core:window:allow-set-resizable", + "core:window:allow-set-size-constraints", + "core:window:allow-center", + "core:window:allow-current-monitor", + "core:tray:default", + "process:default", + { + "identifier": "opener:allow-open-url", + "allow": [{ "url": "https://*" }, { "url": "http://*" }, { "url": "mailto:*" }] + }, + "updater:default" + ] +} diff --git a/studio/src-tauri/icons/128x128.png b/studio/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..c1dae04e6dfaf806cbbb4dc6ea25c1e38276f07e GIT binary patch literal 9194 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 literal 0 HcmV?d00001 diff --git a/studio/src-tauri/icons/32x32.png b/studio/src-tauri/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..bd08d7bcbb6badbdb5abc9a230b5f45d357e1a46 GIT binary patch literal 1930 zcmV;52X**~P)wL)U(wuW>WG;7>4 z|5&MwS&WIZ$;ONzYgwnG)}D!!*E_j8?(O&6^L&r{2@0mOYx-O+_x*i; z-|zE$p7-Z;woCO2zCzM{Q3e|NFe!L4J)|c_uA!@n0=It>zEA*?M)TzZU?CB~gB}-T zorE>X=}4X*kI6s)dP(~~1JQk6!E9ld&x=Vpi22resL9!c4WSr(+W8M8FI|RLldNbR z=!3;zMU=tBzJaVLUmO5y>?Famq`}723>3$uV3psDp11yhijybd82$`(wLisgst=)g zK^i(-4z%3<5SoyJ7^4|MMSgq?JuVBhBz(wwAJ2Y$8IC69z}eD^znps;9o=2HOM;p7 zdOh?;BjTc>QJ9yHk{#c}{NiWu+TC8X-R?(%W!_^!%n5*nv!APTzJ=#MQBZy8$GG&JK{uLOf$S2%b-4*;Do`vl4|3h;E71KZ1X ztEOgR!K}H40#lMC@ypM=FU#_bvpXvGBD3=oRAv+kSI!1Pdbp@~gMOsh5>OdP#E#NZ zv2QRK5TG<;AxwFBc|v;@$iD{<9>mU_JH<1f;j@M*5HHYv+2jH&wJj99BZX^L09w+< z8Svp~dLiC;`4{le+GH}p@Ao4*IvS+}sK37-$B!Qu+Lx4+hjy-$! zz-%_dPayd2Bo(ROz5FVUq!++N{In0%2{j0L+aC%dDb|M9HOukswMEeBbqEH6SigQf znwy&iL#Ba&0l3|6csw5D=jY?rty@S-OH-Nh{+2CU(9+UEJoLmPfJ>cM@RIZqJ{=u` zQL9r6xAdUcNVrc3i<8pv_dowN>~9jDm6e6o)>aG-4&vIiYhph&H5HkenaIh>!MSti zkeHaL3gDAfUiLS0LR8>`pSg_meICbijc;|FF#h0nEu@PRcR{&eHW{o)4 zK|=W)6Z4EE-Yg@MEi(Xc-8ShBkneji?i>$;B4&}4;!7136&M>ELqbA=*h@)C5oTtx zSS-RKH8nNp>FE)f!@`*d)7bbpLQXgJ3M0vqXXyn>I1bon!Kq7}H4BfJdGFr6XlrXjLqmfQ%+`wV;8YEei~K&H$Q~u)^63CHqI=2E5pq3g0tyzs zxMUT-4Ro3IB73+`D%Vr%#_20LDq%(a}+C+qMl3 zheNF;2FfDYCC{h?(*RHrBGj5FGieo#F|S)_!VEa~%gf8vOPrRqwY6}$Tmq!cW+azIKb}rs|uLmQ8;f}|?A08gY zv17-?^Xk>B(bUu=+{3ZKg=adkg$^G+tj5v&1vcboWFufP&;;d!*dwB21C{&Ly3T&9uC7L5VIeMDxF9s=JtMW@i13Mt30%B*QCNqC^ZQ5uSC*Bd z;qBk!)SY)>q^U`ns~z*?!^21B;#Aa*#pAo*+@uB(M|h+c7)UVpg9I`))6mcmdU|`| zo19czHJ{Z`T~TBOfBIW98WP9R`|-aKYqHFklx9ps{36z9L7!s)a&{J0KeY^fH~)nd zD_7#f4?YkY@tL(bd1$BlFrA)TTL61+k65#7B6n7#EJbMfVqEXLjyRL`k>TW#{>a$m zkc@ZygV?`6?1rmt?Ko2P6Tx`*4_?3@?N@P(ToR!pWb!6^C(3s37H3}k;g8UDHC#>i z@7ss=&~SKum~HT}>?;2GOaKZjIz4WTI8eUfJ6Pt{LYt9{6~%=}@k-cIa}Y&+XYtba zcj4~kZlo<*jD3x-p|JZc{H&xDOM@nCI(`Ta=Lk}LdaSD2g;zdmheguQ`rl#Jk&ONB zqt0j}lO4@?4rgy(#~qq#UZla|{H=GUeBWxvQWu(2mESGgvh*B z+Vhb4U49R~X0#$id3B!xn%4%Z1#^w)kDZD&Q}uyUlrw?XoWK0xj0rat9?djD08cwS zaPS$ftTeERCZ{~<{{p@^J!+?~g+^1;O+-5e7L2TwaBcb0{u}n+a07*qoM6N<$f@%`GBLDyZ literal 0 HcmV?d00001 diff --git a/studio/src-tauri/icons/icon.icns b/studio/src-tauri/icons/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..e67a5e64744a7208847d39bca3943d1e99298549 GIT binary patch literal 263568 zcmeFYWmFtZyRh4XI|P?7KyY{WKycTPAR#~q?gR@A&fp;s+}+*XJrLX>XmATI^Ud>~ zwbwd(@8|dVb3RtDrn{?J>aMG*ZW}8zdlvu+Io8USmj?jQD9Z^!L`2DiNTh@qIK&Br z=)|OiAbcqf95Ui>gxJKyXaK%POM3fj0E3t~nh=*19aTBDBkUy*I1{ddhE5tu_<{sM zx-Q><0sxRPDBtBbz6Ozo6C#lS?ma>f0E7ez0FZ>cT|p#aglMFoz!CsJLPCT903rkl z832j{0MZab5>iCdTKN4)002S&0RX~3MV01=q(OuzM7Rw77yt_V4u1IKY~Div6chlh zj}ezBkdTZNNHzh`0PJ`3l?ckfG_1=Q9e!$*3?dC6#3jOIZUm43=^Geqb!rG$fxytN zWZ@SYS#gN`3DHRq?BNgPhHVT&U^oxOe_^n}O&DAvzIF&C{)AYh2x)HsKuiQcR=IzK zz%U=X{Yzk{c zYpU4E`tQtUY*#jgG?Fw`E|vaIW{Z|9n}QpW>w0IA{~NM!X(3Mc)&(_^HC66w{cmD5 z_A8qL8*%FT2jI;7zko%B1B(pw4b%m|+gQ2o@o&7ktyebrH)1taZwvn$uY%jvO#zMM zO-m&IF!n!jC0c4~@NcAOXdg%WH(ZfNJL>%#k?T8#kpBf&P+u3k6n^th^nc?Ob*!U4 zun~EQ=-+6?U;O9ANKMt7@+AL@RwC)G8hD#9o9g!OZ%P_2wVDr<=j@4rOF*o0F`;yh~1&iO!4WW%hP4(^jeXV=FRlO@~JyqR%9liVQ zb^p|Fs;{W8uc<Yi@$?{yDC1ih%Rh)Y#P6*woPQ&%Uv#X{WIq3JMQTh9X6T1q6pD zK+(e^!oG$Vr3Ocae}iI&hx>nx=$p>!E%*`;9v%(FjR*)V|2gkZ4EP=lmj?z!L_%Lg z_!cbYX%PRzE172Aa>3V#aQM@{_V#f77q8qsJ-!iPP_&4z!@2*R*X-f15g|~L2*0NC z|H*4fi*G~_6eT?P$0X{10~Q4tK%Php4iAKqMfgtZ{cmD*rhFp;p*X?GJ#eZ2FJik> zf&-xF5x$*X|3<7wr*DKm6f44SQvBbD6`6!Y1VG6`$I1VVSd#Iu04PORR4>NA0gKub z6Y38|4vXnP{TE;nTff0e;WzhT{u{99y)mHyP?XSd(*Hp$%0Kff;TZhIND+_;72^K| zETP<#f4DysGdw8k&%XfsCo3o%-lA~-+~)rYEC^^QfP@FZh2=lP^dDfw;J}Lh2Uxo4 zfQaCKI<7qmICKDv{tH;k6Ce~W!cZtd=#1UilEbg)H36aZs9%or6SlMAVNjy*kZ-@z zW5!bal7CGlLK4T~(tpJT!wU$)Lw$nbTSRa;G&mx}Cp6e6G&}+hTsRcI!7IRb5#iwx zJ5XC8I~zDfXt4kL_kShuev$|3r=zCn%w7$ij-5<-pT0sF0fE7=!tqwp}(|den;69-Xu$w+~J<9i1GFr9PYp-^8!q zrEj`d8m4Qgr3n=$;(t-z=!5bC@DnGLE+_%xPe90NvyTj5gkM2~7ZF$j@S;sD2m$;G z$Z_-k%gW|~c?;~)Pd`Xw-m1{Ul$Cn!JH=RcpXjn-r3y`+HSkXzc9`e

r@5j#1yz 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 z%0D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.!=!G~*URb_NKZ`~Di5 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 RT{H-IX{qVnFITY%`)_e+9*_V4 literal 0 HcmV?d00001 diff --git a/studio/src-tauri/icons/icon.ico b/studio/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..acc6eb82b9b52829178ffd2943fcfcceec6ec0ac GIT binary patch literal 34589 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 z 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 W#)$R{xljq&_RAOaj{moB@P7dEKw~lh literal 0 HcmV?d00001 diff --git a/studio/src-tauri/icons/icon.png b/studio/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..635900d8a7edc917157d5e1ac9552763950d128f GIT binary patch literal 46007 zcmdSAWmuHo7d1RH3=J}%2#C@kAyU#UNJ}?T0@5uVGlT*Pg3=8lN_R;OAl==i(&5lK z%se;0|F`%1`&`%aMP<0W=bU}^z4zK{trMlGu1HGsfCvNvkt!?6YlA=#;GYl>0Uq$z ziSOhU2vi-cEHA6;JHKa&kI>btDvcV9^Bh+C^raI4hTYvbA>C@+Nt+S0oc7Yw* z#R8RuL1kGewfzVQLKz4`p$Om)1_BDu?GFq}p#MH(0RQ*l|G&TdzxiGNpZ)#6wCTuM zIWZovEUZ<@9Sy(H`+${RiDf#L9x4Bzg30>d!M%Pq`y+)Qdxvljmo0RYic-na{o#zJ znH{EN^tiHD!>tGTQZSpDH2X2D-uNI&K>P3q#}(?s2PK04jz5x`;Hs;=F{fY@di}hQ zgFP;zBark!Z(JZ>U7=1_MRlg_(~7~2L6WW@ef3bb?YD2+NJY&No+k+{@BFH`U9-`N z)h2amvEoPPeMyTgDr{AQaB~j9m)ICqa;R(^2nN*xhrr7T&zg=r@*Rx7PxcRf`J}h2 zoNXMFUq^`M=A21r6--yk9PEEkG9g&4F-0N`JCY24z5cKC=vQ6s(Zr|IqrA(8w=-;W zDNQDv>E#BZ+;UO)vM|a(y2+b0%W?NmB?|`Mytr5=iTa8*>rK(|N@K~N4fZ~w4R4sY zweoT&M@s(ecv42?A6B@V zft*cn3N6#<*f}_%qbi{Gg4EBf<#d5*C&f!@E~Y1`J0u?p2>*A+&v$p3``%N#+6I?d zKWJaNeA(Df?v`D64E zIH6F+Nf1ol10MsKa}E!RsV?g49aijH^>_Z3+K0}ZlX~qUA`ncwy06Ge$-#jSy&l)J z>x^v0exJnYg~H^VDF@1>%XZ$qJ=@!@!_rHvYTjNFb3538Mm+2-} z%5y`o-|+UoS>_ilXZd|l>@#0(Ly=syg5l@?mX7W)adxjN@` z3IrM*cq43NWTajF<|Qe5f1y5kxg|ig+^nTpK1Zqe9tW5t`~!F7o${qc7{aER=D%w3 zh4Fx@>0f&@em+J~ZB;rq*yN^G_~1vvNNj2umc? z;0(np864e}IaiRkxZ;keIWs)D@0)q0-SOZAY<(!SioN(7%kT5=)lFmxeQJ^-U{J!K0I0F`J#n>4X68jX@eEjL{&ZM#lj!i9uNTN7`_Kn`@KaSNRhESkmM>a&%w!bJx znw-8r3Or{VuU23l3M8Itek`~pdNtmC>b*aoPa^`sto6m$mLsit?yy2J{Z}qkXiX&$ zuh)+5&w~!)d~;-Q88oDeFS{D?zlB%k^XPd~tvSRY3x_O^P-i_#3Jygx1dmHILd*rW zLLsC_)wUx^hktu^6;Cc@@l=Sf#y|=L!Y+&K?WO!||E0{I#X8A$$!^* ztBHKzed4u?A@nU?(c3{6za!H2^A=-sK1!DtZz4_5|N4^ri02({X)nJUn8$UI}iop_LkbJX$E8KKly;~Z%&SLOfC$*$z@J4NZLYl*vY zMQQIO499WWntx~vv5gNzUcZ0lgt|F?UTzC_=nbJqVs?tlYbK?g=bw?HmrLgEUO3N5 z>+|{^ScqP(#kZ_vH*DQxJJ!((4HZ2-_Hl~q636l{K+b7Q)`GO>oQO)C%WTVjZkTwS zb29d-s0vdiy=5hOS96Dr-~pIi{^CWTLx;)3ivNyX8>-7KtR+dfV7sc1 z%jAcC#@aN8cdHAfj@HX(r$_OZO{^QJvwLeaOi6u3|Mh_IgaB)eu&AW-k7}bM0_{l6 z9qTs(K??0~_v(LNoI_ir8ScJ-jaRn9TI(iw@r^Lwl;hW0&X*^&S`F`@>nK7{^a9ve z$+MJKfrnjY&sV)WE88a2-F3LzYt8BcsuyeAiUM2R^D##687+*ybAmL$YUZBWxb8TEf0X^j$gEHLjHTAmzEIn4t+U++c#Q zny2uSF-3mwJ;TeZ-Eu3b1Mx8FdsyM6xACvs+Hqy^;VW(C=wQR*)jy6O>FJ?dQvx<) zl@6wA9}^xzV{YL#Q(tvpc=BtL zWyv(juRj)N64ErD5&QX1JF%D+&m{Y&;+Oq-c~~&@3!Hu`HyGi8tsar$y33|EI)^bN zL#w;L_V;gm6|i5c0^;ZA550rIbSBx~@z6upeuK7mT!)X~b^t?(`m&H|}uy0dI1?!&K4w9gM80 z;?UkloS!@#1AK`Ysp;4_`nvMO^`<&@z6-CB`#O($g>`6gwTB`_KK2MGLvQhUT&GEE zd=cx7X?K|P(!;YkHYdH|mv@HSx-S1o7|8{TBzk0GAm0YqyWqA2@a7Ee=l55PFg+ej3!3a5YN@JzfvX^JrTOv3P`>}^&Q$;u9u3wndu3FlTlnXO zz#JjwwX@X&{6;wpkM!jH!iRw@Uyl@RnyjQp{Dj=i0ndW} z+u;ljBo-48_4Cpi6e`h+V1*_0Yes>Q-9 zJ5XwZ7Hc2o_+5Maz38A?2s4KGMDT5wV{xdy_WM4FY+^95Z7g@5G8pEY3*P0dRhyIk zMSJp#d!ss}=fg!H4%yWOmqA$qC71q|Vl(FAFPRZCxCQTKC|#JoqT(^*ozEcFpnB=Y z5UC=F?^LOY0ikTMerZhyBG!4nM(eBQ{7l7)TmwM}CE0zySC7C@3}sV&@3+$d>#OgH zRSqNGMpofrsf86G!zT_Z|F!Oplzqq4m4|Ixe`f?eualoi8xA`<^rQ85FRiaRENSOG z@pxC9?#-4U-P|MBq8`0?frTe1th|m2;W7Uc|ipumM zZ~(MDI63TCxvcIcWxcCDf4-oBhe9>}M#&OXyr#C!k&C$FF)EAC!Pv$nATZ(T#qiL& z&4|*iE>Sb4(X;QZpVyQ|@3k69Rry(85F7zXIziQa4#2MUdXj%6`Xm>h8SyYhxOP-;cc))A zdhc^)iu;`D6Ykl4trmj~>xY!P!4?RmpY4ZeA%nQYk31%Ac;y5k�JyHt`&Cdf~3g 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�hCN)??pw5Q7TpEwrkL+zI0Ri{mMStpZ~`uXZ#{+q26Cx zdc9?xH$!;s=mg!=T0J9H6*3MblQI?#Z%URFwqp5&Qxw7)_C#K}M2r5elkhfVMqW0` zH;MGK`JSbxz7FZ6@bhk0WM!AkOwKxCUm$T$wQ(s9n~f^W5fSG(+upyS)vKHFwp3mn z-dJsCjV3Up((s>fU7T^Q+DDQ07yCHI^?~(w+lpTW^roQle6R)4;#iF3?7VU419iMs zGJR{PUD(o}aHF?|86xTM*N9UNi!6<5@HCJZ1=#%k&@f~6;%IeZSlQDR{8QAsMW#vz zrSAp9?%RJ`(x2&_L)%))&CV7(0>f*5t3+88Mh!3PaV-_LvMv4+Vm>Dw2*8U>Vq4p@ z2yNqlvuHA98)PqWetocec;Ka`INBmx8``c%yk2=_cM4 zb<~C}JmE;wn)Hc>SHa9W37Lmr5)}G+hlu~{C1a~{&Vfc2nbA6Jb~75X{v>#Gve5E1or1w zjSHcT1D`A|o%LybmZ#MylkbC5T3Wk^M@?&tq3dt_-K&H3aXX~R(`>9R5nSyjfUm~R z(XU$JrYPTZdNFJ^U43aBcFy&~A1(IlBy|>5Y?ZrBc>S092p9Qe(O2}vByP?0Ql@)| z$*hx6oarhh*#zr8crun+G00tA8NC8A9m@!%tZHyWXqpN*Hcj!n27Q&)C0zBE^)Jqd zlhpYK$ySazdgTPI4Pe+kf@vvb;bagphrjVoa>DGNXN4OtoWs8iJqxa^S5u<%HtfbF zyeSXPGmn8J^*mLj_5x_zw*qe#{@601%*&sw@MUO+KZvGL*u=#;xNI<6d2{4iM?%M- zqfn_gJyB<+w6vPBZ(1FzTD_}STvj6W7jFc!?1Gkd=B{suHGe0@*Gc%f2h62zNu=>-hZQ0%Evq!5?8&NPg8i){H%pVt&! zx-PM=7$}!6BEqLd#PH~Uuv3|IE_N0oMUHf)kVG>|RUC5?qvLL`NTiQ09DNSO^nFlt zHuUFX;DX1*+WR(&+H1nl-X`L*OIw=9Z4y5n##a;Kh~s{U)Fu^XMkbP2CS^oSUaU}?K4ANcG3$ycL#qy_omFpZpel5XO$1^F?H`*DFUySGgw$ZF$r z#k)I3E{Si;#f@4>CUA&3za;~lT}FrhZ3Hl;9o4=}mz_UP^HZj!{=4}SW+6^Mt*INz zm$a(>#&GIz)P)Mc|1?Q9oeInc#NQh+vse~4#aBC)p4H1!WV5c)RVwE;#h0GAYYdt< zCdtRJR9KXX%f;chB1#OfQi$Q6u@Z<@$6Ro2$R`{(s?TlE)-C)Q!p*kQS6kSuFgpJf zjTtf_T8Bum11d8z_}x;o*f^exN$P@{&^Y%pHK|EcYE)1m66-apNaIp#V|Thmb;c0D zrwm!N6zS4hMAs=+Z$@&i(y*yQN$X$tJRd?|6Z%)(%FE1DH1&yJ4A&AT^%jr|4X}LA zHr`ZYr}9=vL6k bool { + crate::preflight::managed_install_ready().await +} + +fn should_emit_repair_failed(msg: &str) -> bool { + !msg.contains("NEEDS_ELEVATION") +} + +#[tauri::command] +pub async fn desktop_preflight() -> crate::preflight::DesktopPreflightResult { + crate::preflight::desktop_preflight_result().await +} + +/// Check if unsloth is installed AND functional. +/// Runs `unsloth -h` to verify the import chain works — a partial install +/// (binary exists but deps missing) will fail on import and return false, +/// which sends the user to the install screen for a clean re-install. +#[tauri::command] +pub async fn check_install_status() -> bool { + let Some(bin) = process::find_unsloth_binary() else { + return false; + }; + + let mut cmd = tokio::process::Command::new(&bin); + cmd.arg("-h") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()); + + #[cfg(windows)] + { + cmd.creation_flags(crate::process::CREATE_NO_WINDOW); + } + + // Match the same AppImage env clearing used in process.rs and install.rs, + // otherwise the probe can fail due to bundled libs even when the install is fine. + #[cfg(target_os = "linux")] + if std::env::var_os("APPIMAGE").is_some() { + cmd.env_remove("LD_LIBRARY_PATH"); + cmd.env_remove("PYTHONHOME"); + cmd.env_remove("PYTHONPATH"); + } + + let mut child = match cmd.spawn() { + Ok(c) => c, + Err(e) => { + warn!("Install check: failed to spawn {:?}: {}", bin, e); + return false; + } + }; + + match tokio::time::timeout(std::time::Duration::from_secs(10), child.wait()).await { + Ok(Ok(status)) => { + let ok = status.success(); + if !ok { + warn!("Install check: `unsloth -h` exited with {}", status); + } + ok + } + Ok(Err(e)) => { + warn!("Install check: wait failed: {}", e); + false + } + Err(_) => { + warn!("Install check: `unsloth -h` timed out after 10s"); + let _ = child.kill().await; + false + } + } +} + +/// Start the backend server on the given port. +/// Also spawns a health watchdog that monitors the backend and emits +/// `server-crashed` if it becomes unresponsive (deadlock, OOM, etc.). +#[tauri::command] +pub async fn start_server( + app: AppHandle, + state: tauri::State<'_, BackendState>, + shutdown: tauri::State<'_, ShutdownFlag>, + port: u16, +) -> Result<(), String> { + info!("start_server command called with port {}", port); + + process::start_backend(&app, &state, port, &shutdown)?; + + // Spawn health watchdog for the owned backend — detects + // deadlocks and hangs that stdout-based crash detection misses. + let watchdog_state = state.inner().clone(); + let watchdog_shutdown = shutdown.inner().clone(); + let watchdog_app = app.clone(); + tokio::spawn(async move { + health_watchdog(watchdog_app, watchdog_state, watchdog_shutdown).await; + }); + + Ok(()) +} + +/// Start the managed backend without reusing an existing backend. +#[tauri::command] +pub async fn start_managed_server( + app: AppHandle, + state: tauri::State<'_, BackendState>, + shutdown: tauri::State<'_, ShutdownFlag>, + port: u16, +) -> Result<(), String> { + info!("start_managed_server command called with port {}", port); + process::start_backend(&app, &state, port, &shutdown)?; + + let watchdog_state = state.inner().clone(); + let watchdog_shutdown = shutdown.inner().clone(); + let watchdog_app = app.clone(); + tokio::spawn(async move { + health_watchdog(watchdog_app, watchdog_state, watchdog_shutdown).await; + }); + + Ok(()) +} + +/// Stop the backend server. +/// Sends SIGTERM to the process group, which triggers uvicorn's graceful +/// shutdown (same codepath as /api/shutdown). Falls back to SIGKILL after 5s. +#[tauri::command] +pub fn stop_server( + state: tauri::State<'_, BackendState>, + shutdown: tauri::State<'_, ShutdownFlag>, +) -> Result<(), String> { + info!("stop_server command called"); + process::stop_backend(&state, &shutdown) +} + +/// Check if a healthy Unsloth backend is running on the given port. +/// Expects JSON response with status=="healthy" AND service=="Unsloth UI Backend". +#[tauri::command] +pub async fn check_health(port: u16) -> Result { + match check_health_inner(port).await { + Ok(healthy) => Ok(healthy), + Err(e) => { + // Network errors are not command errors — just means not healthy + info!("Health check on port {} failed: {}", port, e); + Ok(false) + } + } +} + +async fn check_health_inner(port: u16) -> Result { + let url = format!("http://127.0.0.1:{}/api/health", port); + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(2)) + .build()?; + let resp = client.get(&url).send().await?; + let json: serde_json::Value = resp.json().await?; + + let healthy = json + .get("status") + .and_then(|v| v.as_str()) + .map(|s| s == "healthy") + .unwrap_or(false); + let correct_service = json + .get("service") + .and_then(|v| v.as_str()) + .map(|s| s == "Unsloth UI Backend") + .unwrap_or(false); + + Ok(healthy && correct_service) +} + +/// Return buffered server logs. +#[tauri::command] +pub fn get_server_logs(state: tauri::State<'_, BackendState>) -> Vec { + match state.lock() { + Ok(proc) => proc.logs.iter().cloned().collect(), + Err(e) => { + error!("Failed to lock state for logs: {}", e); + vec![] + } + } +} + +/// Open the Unsloth Studio directory in the system file manager. +#[tauri::command] +pub fn open_logs_dir() -> Result<(), String> { + let home = dirs::home_dir().ok_or("Could not determine home directory")?; + let dir = home.join(".unsloth").join("studio"); + + if !dir.exists() { + return Err(format!("Directory does not exist: {}", dir.display())); + } + + open::that(&dir).map_err(|e| format!("Failed to open directory: {}", e)) +} + +/// Start the first-launch installation process. +/// Runs the platform installer script with --tauri flag and streams progress events. +/// Returns "NEEDS_ELEVATION" if system packages need elevated install (Linux only). +#[tauri::command] +pub async fn start_install( + app: AppHandle, + state: tauri::State<'_, install::InstallState>, +) -> Result<(), String> { + let state = state.inner().clone(); + tokio::task::spawn_blocking(move || install::run_install(app, state)) + .await + .map_err(|e| format!("Install task panicked: {e}"))? +} + +/// Install system packages with elevated permissions (Linux only). +/// Called by frontend after user approves the elevation dialog. +/// Only allows packages that the install script reported as needed. +#[cfg(target_os = "linux")] +#[tauri::command] +pub fn install_system_packages( + packages: Vec, + state: tauri::State<'_, install::InstallState>, +) -> Result<(), String> { + // Cross-check against the packages the install script actually reported + let allowed = state + .lock() + .map(|s| s.needed_packages.clone()) + .unwrap_or_default(); + for pkg in &packages { + if !allowed.contains(pkg) { + return Err(format!( + "Package '{}' was not requested by the install script", + pkg + )); + } + } + install::install_system_packages(&packages) +} + +/// Stub for non-Linux platforms — elevation is handled by the scripts themselves. +#[cfg(not(target_os = "linux"))] +#[tauri::command] +pub fn install_system_packages( + _packages: Vec, + _state: tauri::State<'_, install::InstallState>, +) -> Result<(), String> { + Err("Elevated package install is only supported on Linux".to_string()) +} + +/// Run backend update: stop server, run `unsloth studio update`, emit progress. +/// Does NOT restart the backend — the frontend handles shell update + relaunch after. +#[tauri::command] +pub async fn start_backend_update( + app: AppHandle, + backend_state: tauri::State<'_, BackendState>, + shutdown: tauri::State<'_, ShutdownFlag>, + update_state: tauri::State<'_, update::UpdateState>, + install_state: tauri::State<'_, install::InstallState>, +) -> Result<(), String> { + info!("start_backend_update command called"); + + // Signal the health watchdog to exit immediately, before any guards. + // This closes the race window where the watchdog could emit server-crashed + // between our command being called and stop_backend completing. + shutdown.store(true, std::sync::atomic::Ordering::SeqCst); + + // Guard: reject if install is running + if install_state + .lock() + .map(|s| s.child.is_some()) + .unwrap_or(false) + { + return Err("Cannot update while installation is in progress.".to_string()); + } + + // Guard: reject if update is already running + if update_state + .lock() + .map(|s| s.child.is_some()) + .unwrap_or(false) + { + return Err("Update is already running.".to_string()); + } + + // Stop backend if running + if backend_state + .lock() + .map(|s| s.child.is_some()) + .unwrap_or(false) + { + info!("Stopping backend before update..."); + process::stop_backend(&backend_state, &shutdown)?; + } + + // Run update in a blocking thread + let state = update_state.inner().clone(); + tokio::task::spawn_blocking(move || update::run_backend_update(app, state)) + .await + .map_err(|e| format!("Update task panicked: {e}"))? +} + +/// Repair a stale managed Studio install. +#[tauri::command] +pub async fn start_managed_repair( + app: AppHandle, + backend_state: tauri::State<'_, BackendState>, + shutdown: tauri::State<'_, ShutdownFlag>, + update_state: tauri::State<'_, update::UpdateState>, + install_state: tauri::State<'_, install::InstallState>, +) -> Result<(), String> { + info!("start_managed_repair command called"); + + if install_state + .lock() + .map(|s| s.child.is_some()) + .unwrap_or(false) + { + return Err("Cannot repair while installation is in progress.".to_string()); + } + + if update_state + .lock() + .map(|s| s.child.is_some()) + .unwrap_or(false) + { + return Err("Repair is already running.".to_string()); + } + + shutdown.store(true, std::sync::atomic::Ordering::SeqCst); + + if backend_state + .lock() + .map(|s| s.child.is_some()) + .unwrap_or(false) + { + info!("Stopping backend before repair..."); + process::stop_backend(&backend_state, &shutdown)?; + } + + let _ = app.emit("repair-progress", "Updating existing Studio install..."); + let update_app = app.clone(); + let update_state = update_state.inner().clone(); + let update_result = tokio::task::spawn_blocking(move || { + update::run_backend_update_for_repair(update_app, update_state) + }) + .await + .map_err(|e| format!("Repair update task panicked: {e}"))?; + + match update_result { + Ok(()) if managed_install_ready_after_repair().await => { + info!("Managed repair complete after update"); + let _ = app.emit("repair-complete", ()); + return Ok(()); + } + Ok(()) => { + warn!("Managed repair update finished, but preflight is still not ready; falling back to installer"); + let _ = app.emit( + "repair-progress", + "Update finished, but Studio is still not ready. Running bundled installer...", + ); + } + Err(msg) => { + if msg.to_ascii_lowercase().contains("already running") { + error!("Managed repair update conflict: {}", msg); + let _ = app.emit("repair-failed", &msg); + return Err(msg); + } + + warn!( + "Managed repair update failed, falling back to bundled installer: {}", + msg + ); + let _ = app.emit( + "repair-progress", + "Update failed. Running bundled installer...", + ); + } + } + + let install_app = app.clone(); + let install_state = install_state.inner().clone(); + let install_result = tokio::task::spawn_blocking(move || { + install::run_install_for_repair(install_app, install_state) + }) + .await + .map_err(|e| format!("Repair install task panicked: {e}"))?; + + if let Err(msg) = install_result { + if should_emit_repair_failed(&msg) { + error!("Managed repair installer failed: {}", msg); + let _ = app.emit("repair-failed", &msg); + } + return Err(msg); + } + + if managed_install_ready_after_repair().await { + info!("Managed repair complete after installer"); + let _ = app.emit("repair-complete", ()); + return Ok(()); + } + + let msg = "Repair finished, but Studio install is still not desktop-ready.".to_string(); + error!("{}", msg); + let _ = app.emit("repair-failed", &msg); + Err(msg) +} + +#[cfg(test)] +mod tests { + #[test] + fn repair_elevation_is_not_a_terminal_repair_failure() { + assert!(!super::should_emit_repair_failed("NEEDS_ELEVATION")); + assert!(super::should_emit_repair_failed( + "Installer exited with code 1" + )); + } +} + +/// Periodic health check that detects deadlocked or hung backends. +/// Starts 30s after the backend is launched (to allow initial startup), +/// then pings /api/health every 15s. After 3 consecutive failures (45s) +/// with the process still alive, emits `server-crashed` so the frontend +/// can offer a restart. +async fn health_watchdog(app: AppHandle, state: BackendState, shutdown: ShutdownFlag) { + use std::sync::atomic::Ordering; + + // Give the backend time to start up + tokio::time::sleep(std::time::Duration::from_secs(30)).await; + + let mut consecutive_failures: u32 = 0; + + loop { + tokio::time::sleep(std::time::Duration::from_secs(15)).await; + + if shutdown.load(Ordering::SeqCst) { + info!("Health watchdog: shutdown flag set, exiting"); + break; + } + + let (port, has_child) = { + let proc = match state.lock() { + Ok(p) => p, + Err(_) => break, + }; + (proc.port, proc.child.is_some()) + }; + + // Stop watching if the backend is gone + if !has_child { + info!("Health watchdog: backend stopped, exiting"); + break; + } + + let Some(port) = port else { + continue; // Port not yet known + }; + + match check_health_inner(port).await { + Ok(true) => { + consecutive_failures = 0; + } + _ => { + consecutive_failures += 1; + warn!( + "Health watchdog: failure {}/3 on port {}", + consecutive_failures, port + ); + if consecutive_failures >= 3 { + error!( + "Health watchdog: backend unresponsive for 45s, killing and declaring dead" + ); + // Kill the zombie process so retry can start fresh + let _ = process::stop_backend(&state, &shutdown); + let _ = app.emit("server-crashed", ()); + break; + } + } + } + } +} diff --git a/studio/src-tauri/src/desktop_auth.rs b/studio/src-tauri/src/desktop_auth.rs new file mode 100644 index 0000000000..df09df7a14 --- /dev/null +++ b/studio/src-tauri/src/desktop_auth.rs @@ -0,0 +1,388 @@ +use crate::preflight::{DesktopPreflightDisposition, DesktopPreflightResult}; +use crate::process::BackendState; +use log::info; +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; + +static DESKTOP_AUTH_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); + +#[derive(Debug, Serialize)] +pub struct DesktopAuthResponse { + pub access_token: String, + pub refresh_token: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct DesktopAuthRequest { + secret: String, +} + +#[derive(Debug, Deserialize)] +struct TokenResponse { + access_token: String, + refresh_token: String, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum PortSource { + Cached, + Discovered, +} + +#[derive(Clone, Copy, Debug)] +struct BackendPort { + port: u16, + source: PortSource, +} + +#[derive(Debug)] +enum AuthError { + Connectivity(String), + Failed(String), +} + +impl AuthError { + fn message(self) -> String { + match self { + Self::Connectivity(message) | Self::Failed(message) => message, + } + } +} + +fn auth_secret_path(home: &Path, filename: &str) -> PathBuf { + home.join(".unsloth") + .join("studio") + .join("auth") + .join(filename) +} + +fn auth_url(port: u16, route: &str) -> String { + format!("http://127.0.0.1:{port}/api/auth/{route}") +} + +fn home_dir() -> Result { + dirs::home_dir().ok_or_else(|| "Could not determine home directory".to_string()) +} + +fn desktop_secret_path() -> Result { + Ok(auth_secret_path(&home_dir()?, ".desktop_secret")) +} + +fn read_secret_if_exists(path: &Path) -> Result, String> { + match std::fs::read_to_string(path) { + Ok(s) => Ok(Some(s.trim().to_string())), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(e) => Err(format!( + "Failed to read auth secret at {}: {}", + path.display(), + e + )), + } +} + +async fn current_backend_port( + state: &tauri::State<'_, BackendState>, +) -> Result { + if let Some(port) = state.lock().map_err(|e| e.to_string())?.port { + return Ok(BackendPort { + port, + source: PortSource::Cached, + }); + } + + let port = discover_compatible_backend_port() + .await + .ok_or_else(|| "Backend is not ready".to_string())?; + + { + let mut proc = state.lock().map_err(|e| e.to_string())?; + if proc.port.is_none() { + proc.port = Some(port); + } + } + + Ok(BackendPort { + port, + source: PortSource::Discovered, + }) +} + +fn attached_ready_port(preflight: DesktopPreflightResult) -> Option { + if preflight.disposition == DesktopPreflightDisposition::AttachedReady { + preflight.port + } else { + None + } +} + +async fn discover_compatible_backend_port() -> Option { + attached_ready_port(crate::preflight::desktop_preflight_result().await) +} + +fn update_backend_port(state: &tauri::State<'_, BackendState>, port: u16) -> Result<(), String> { + let mut proc = state.lock().map_err(|e| e.to_string())?; + proc.port = Some(port); + Ok(()) +} + +fn classify_auth_send_error(error: reqwest::Error) -> AuthError { + let message = format!("Desktop auth failed: {}", error); + if error.is_connect() || error.is_timeout() { + AuthError::Connectivity(message) + } else { + AuthError::Failed(message) + } +} + +fn should_retry_with_discovered_port(source: PortSource, error: &AuthError) -> bool { + matches!( + (source, error), + (PortSource::Cached, AuthError::Connectivity(_)) + ) +} + +async fn exchange_desktop_secret( + client: &Client, + port: u16, + secret: &str, +) -> Result, AuthError> { + let response = client + .post(auth_url(port, "desktop-login")) + .json(&DesktopAuthRequest { + secret: secret.to_string(), + }) + .send() + .await + .map_err(classify_auth_send_error)?; + + if response.status() == reqwest::StatusCode::NOT_FOUND { + return Err(AuthError::Failed( + "Running Studio backend is too old for this desktop app. Update that backend and restart." + .to_string(), + )); + } + if response.status() == reqwest::StatusCode::UNAUTHORIZED { + return Ok(None); + } + if !response.status().is_success() { + return Err(AuthError::Failed("Desktop auth failed".to_string())); + } + + response + .json::() + .await + .map(|tokens| { + Some(DesktopAuthResponse { + access_token: tokens.access_token, + refresh_token: tokens.refresh_token, + }) + }) + .map_err(|e| AuthError::Failed(format!("Desktop auth failed: {}", e))) +} + +async fn provision_desktop_auth() -> Result<(), String> { + let bin = crate::process::resolve_backend_binary()?; + let mut cmd = tokio::process::Command::new(&bin); + cmd.args(["studio", "provision-desktop-auth"]) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::piped()); + #[cfg(target_os = "linux")] + if std::env::var_os("APPIMAGE").is_some() { + cmd.env_remove("LD_LIBRARY_PATH"); + cmd.env_remove("PYTHONHOME"); + cmd.env_remove("PYTHONPATH"); + } + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + cmd.creation_flags(crate::process::CREATE_NO_WINDOW); + } + + let output = tokio::time::timeout(std::time::Duration::from_secs(30), cmd.output()) + .await + .map_err(|_| "Desktop auth provisioning timed out after 30s".to_string())? + .map_err(|e| format!("Desktop auth provisioning failed: {}", e))?; + if output.status.success() { + return Ok(()); + } + let stderr = String::from_utf8_lossy(&output.stderr); + Err(format!( + "Desktop auth provisioning failed: {}", + stderr.trim() + )) +} + +async fn authenticate_with_stale_port_retry( + client: &Client, + state: &tauri::State<'_, BackendState>, + backend: BackendPort, + secret: &str, +) -> Result<(Option, BackendPort), String> { + match exchange_desktop_secret(client, backend.port, secret).await { + Ok(tokens) => Ok((tokens, backend)), + Err(error) if should_retry_with_discovered_port(backend.source, &error) => { + let Some(port) = discover_compatible_backend_port().await else { + return Err(error.message()); + }; + update_backend_port(state, port)?; + let backend = BackendPort { + port, + source: PortSource::Discovered, + }; + exchange_desktop_secret(client, port, secret) + .await + .map(|tokens| (tokens, backend)) + .map_err(AuthError::message) + } + Err(error) => Err(error.message()), + } +} + +#[tauri::command] +pub async fn desktop_auth( + state: tauri::State<'_, BackendState>, +) -> Result { + let _auth_guard = DESKTOP_AUTH_LOCK.lock().await; + let mut backend = current_backend_port(&state).await?; + let client = Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .build() + .map_err(|e| format!("Desktop auth failed: {}", e))?; + + for attempt in 0..2 { + if attempt == 1 { + info!("Desktop auth: provisioning local desktop secret"); + provision_desktop_auth().await?; + } + + let path = desktop_secret_path()?; + let Some(secret) = read_secret_if_exists(&path)? else { + continue; + }; + + info!("Desktop auth: exchanging desktop secret"); + let (tokens, resolved_backend) = + authenticate_with_stale_port_retry(&client, &state, backend, &secret).await?; + backend = resolved_backend; + if let Some(tokens) = tokens { + return Ok(tokens); + } + } + + Err( + "Desktop auth failed. Update or repair the managed Studio install, then restart Studio." + .to_string(), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::net::TcpListener; + + async fn login_server(status: &str) -> u16 { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + let status = status.to_string(); + + tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + let mut buffer = [0; 1024]; + let _ = stream.read(&mut buffer).await.unwrap(); + let response = + format!("HTTP/1.1 {status}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"); + stream.write_all(response.as_bytes()).await.unwrap(); + }); + + port + } + + #[test] + fn auth_secret_path_joins_expected_location() { + let home = PathBuf::from("/home/alex"); + assert_eq!( + auth_secret_path(&home, ".desktop_secret"), + PathBuf::from("/home/alex/.unsloth/studio/auth/.desktop_secret") + ); + } + + #[test] + fn auth_url_builds_local_endpoint() { + assert_eq!( + auth_url(8890, "desktop-login"), + "http://127.0.0.1:8890/api/auth/desktop-login" + ); + } + + #[test] + fn retry_discovery_only_for_cached_connectivity_errors() { + assert!(should_retry_with_discovered_port( + PortSource::Cached, + &AuthError::Connectivity("connection refused".to_string()) + )); + assert!(!should_retry_with_discovered_port( + PortSource::Discovered, + &AuthError::Connectivity("connection refused".to_string()) + )); + assert!(!should_retry_with_discovered_port( + PortSource::Cached, + &AuthError::Failed("Desktop auth failed".to_string()) + )); + } + + #[test] + fn attached_ready_port_requires_attached_ready_with_port() { + let compatible = DesktopPreflightResult { + disposition: DesktopPreflightDisposition::AttachedReady, + reason: None, + port: Some(8890), + can_auto_repair: false, + managed_bin: None, + }; + assert_eq!(attached_ready_port(compatible), Some(8890)); + + let missing_port = DesktopPreflightResult { + disposition: DesktopPreflightDisposition::AttachedReady, + reason: None, + port: None, + can_auto_repair: false, + managed_bin: None, + }; + assert_eq!(attached_ready_port(missing_port), None); + + let managed_ready = DesktopPreflightResult { + disposition: DesktopPreflightDisposition::ManagedReady, + reason: None, + port: Some(8890), + can_auto_repair: false, + managed_bin: None, + }; + assert_eq!(attached_ready_port(managed_ready), None); + } + + #[tokio::test] + async fn exchange_desktop_secret_returns_none_for_unauthorized() { + let port = login_server("401 Unauthorized").await; + let tokens = exchange_desktop_secret(&Client::new(), port, "desktop-stale") + .await + .unwrap(); + + assert!(tokens.is_none()); + } + + #[tokio::test] + async fn exchange_desktop_secret_reports_unsupported_backend_on_not_found() { + let port = login_server("404 Not Found").await; + let error = exchange_desktop_secret(&Client::new(), port, "desktop-secret") + .await + .unwrap_err() + .message(); + + assert_eq!( + error, + "Running Studio backend is too old for this desktop app. Update that backend and restart." + ); + } +} diff --git a/studio/src-tauri/src/install.rs b/studio/src-tauri/src/install.rs new file mode 100644 index 0000000000..5760989353 --- /dev/null +++ b/studio/src-tauri/src/install.rs @@ -0,0 +1,555 @@ +use log::{error, info, warn}; +use process_wrap::std::*; +use std::io::BufRead; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitStatus, Stdio}; +use std::sync::{Arc, Mutex}; +use tauri::{AppHandle, Emitter, Manager}; + +// ── Types ── + +pub struct InstallProcess { + /// Process group handle — killing this kills the entire subprocess tree. + pub child: Option>, + pub intentional_stop: bool, + /// Packages needing elevated install, parsed from [TAURI:NEED_SUDO] output. + pub needed_packages: Vec, +} + +impl Default for InstallProcess { + fn default() -> Self { + Self { + child: None, + intentional_stop: false, + needed_packages: Vec::new(), + } + } +} + +pub type InstallState = Arc>; + +pub fn new_install_state() -> InstallState { + Arc::new(Mutex::new(InstallProcess::default())) +} + +use crate::process::trim_line_endings; + +// ── Script Resolution ── + +/// Returns (script_path, args) depending on dev vs production mode. +/// Dev mode: repo root script + --tauri --local +/// Production: bundled resource + --tauri +fn resolve_install_script(app: &AppHandle) -> Result<(PathBuf, Vec), String> { + let mut args = vec!["--tauri".to_string()]; + + if cfg!(debug_assertions) { + let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() // studio/ + .and_then(|p| p.parent()) // repo root + .ok_or("Cannot resolve repo root from CARGO_MANIFEST_DIR")?; + + let script = if cfg!(unix) { + repo_root.join("install.sh") + } else { + repo_root.join("install.ps1") + }; + + if !script.exists() { + return Err(format!("Install script not found: {}", script.display())); + } + + args.push("--local".to_string()); + info!("Dev mode: using repo script at {}", script.display()); + Ok((script, args)) + } else { + let name = if cfg!(unix) { + "install.sh" + } else { + "install.ps1" + }; + let script = app + .path() + .resolve(name, tauri::path::BaseDirectory::Resource) + .map_err(|e| format!("Failed to resolve bundled {}: {}", name, e))?; + info!("Production: using bundled script at {}", script.display()); + Ok((script, args)) + } +} + +// ── Emit Helpers ── + +#[derive(Clone, Copy)] +enum InstallEventMode { + Full, + Repair, +} + +impl InstallEventMode { + fn progress_event(self) -> &'static str { + match self { + Self::Full => "install-progress", + Self::Repair => "repair-progress", + } + } + + fn emit_install_structured_events(self) -> bool { + matches!(self, Self::Full) + } + + fn emit_terminal_events(self) -> bool { + matches!(self, Self::Full) + } + + fn needs_elevation_event(self) -> &'static str { + match self { + Self::Full => "install-needs-elevation", + Self::Repair => "repair-needs-elevation", + } + } +} + +fn emit_mode_progress(app: &AppHandle, mode: InstallEventMode, message: &str) { + info!("[install] {}", message); + let _ = app.emit(mode.progress_event(), message); +} + +fn emit_failed(app: &AppHandle, message: &str) { + error!("[install] FAILED: {}", message); + let _ = app.emit("install-failed", message); +} + +fn emit_complete(app: &AppHandle) { + info!("[install] Installation complete"); + let _ = app.emit("install-complete", ()); +} + +// ── Spawn ── + +/// Spawns the install script in a process group. +/// Returns (stdout, stderr) handles for streaming. +/// The GroupChild is stored in state so stop_install() can kill the entire tree. +fn spawn_script( + script: &Path, + args: &[String], + state: &InstallState, +) -> Result< + ( + Option, + Option, + ), + String, +> { + let mut install = state.lock().map_err(|e| e.to_string())?; + if install.child.is_some() { + return Err("Installation is already running.".to_string()); + } + install.intentional_stop = false; + install.needed_packages.clear(); + + // Scripts create ~/.unsloth/studio/ themselves, but need a writable cwd. + let home = dirs::home_dir().ok_or("Could not determine home directory")?; + let work_dir = home.join(".unsloth"); + if !work_dir.exists() { + std::fs::create_dir_all(&work_dir) + .map_err(|e| format!("Failed to create {}: {}", work_dir.display(), e))?; + } + + #[cfg(unix)] + let mut cmd = Command::new("bash"); + #[cfg(unix)] + cmd.arg(script) + .args(args) + .current_dir(&work_dir) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + #[cfg(windows)] + let mut cmd = Command::new("powershell.exe"); + #[cfg(windows)] + cmd.args([ + "-NoLogo", + "-NoProfile", + "-NonInteractive", + "-WindowStyle", + "Hidden", + "-ExecutionPolicy", + "Bypass", + "-File", + ]) + .arg(script) + .args(args) + .current_dir(&work_dir) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + // AppImage sets LD_LIBRARY_PATH to its bundled libs, which breaks Python + // spawned by the install script. Only clear inside AppImage — native installs + // may need these for custom CUDA or conda paths. + #[cfg(target_os = "linux")] + if std::env::var_os("APPIMAGE").is_some() { + cmd.env_remove("LD_LIBRARY_PATH"); + cmd.env_remove("PYTHONHOME"); + cmd.env_remove("PYTHONPATH"); + } + + // On Windows, launch the installer directly with CREATE_NO_WINDOW. + // The app process is assigned to a KILL_ON_JOB_CLOSE job in main.rs, so + // child cleanup on crash comes from inherited job membership instead. + #[cfg(windows)] + let mut child: Box = { + use std::os::windows::process::CommandExt; + cmd.creation_flags(crate::process::CREATE_NO_WINDOW); + let child = cmd + .spawn() + .map_err(|e| format!("Failed to spawn install script: {}", e))?; + Box::new(child) + }; + + #[cfg(unix)] + let mut child: Box = { + // Keep the whole installer tree in a process group on Unix. + let mut wrap = CommandWrap::from(cmd); + wrap.wrap(ProcessGroup::leader()); + wrap.spawn() + .map_err(|e| format!("Failed to spawn install script: {}", e))? + }; + + let stdout = child.stdout().take(); + let stderr = child.stderr().take(); + install.child = Some(child); + Ok((stdout, stderr)) +} + +// ── Stream ── + +/// Spawns reader threads for stdout/stderr. +/// Parses [TAURI:*] lines from stdout for structured events. +fn stream_output( + app: &AppHandle, + state: &InstallState, + event_mode: InstallEventMode, + stdout: Option, + stderr: Option, +) -> Vec> { + let mut threads = Vec::new(); + + if let Some(out) = stdout { + let app_clone = app.clone(); + let state_clone = Arc::clone(state); + threads.push(std::thread::spawn(move || { + let mut reader = std::io::BufReader::new(out); + let mut buf = Vec::new(); + loop { + buf.clear(); + match reader.read_until(b'\n', &mut buf) { + Ok(0) => break, + Ok(_) => { + let text = String::from_utf8_lossy(trim_line_endings(&buf)).into_owned(); + // Parse structured Tauri protocol lines + if let Some(packages) = text.strip_prefix("[TAURI:NEED_SUDO] ") { + let pkgs: Vec = + packages.split_whitespace().map(String::from).collect(); + if let Ok(mut install) = state_clone.lock() { + install.needed_packages = pkgs; + } + } else if let Some(step) = text.strip_prefix("[TAURI:STEP] ") { + if !event_mode.emit_install_structured_events() { + info!("[install][stdout] {}", text); + let _ = app_clone.emit(event_mode.progress_event(), &text); + continue; + } + let _ = app_clone.emit("install-step", step); + } else if let Some(detail) = text.strip_prefix("[TAURI:PROGRESS] ") { + if !event_mode.emit_install_structured_events() { + info!("[install][stdout] {}", text); + let _ = app_clone.emit(event_mode.progress_event(), detail); + continue; + } + let _ = app_clone.emit("install-progress-detail", detail); + } + // Always forward the raw line + info!("[install][stdout] {}", text); + let _ = app_clone.emit(event_mode.progress_event(), &text); + } + Err(e) => { + warn!("[install] Error reading stdout: {}", e); + break; + } + } + } + })); + } + + if let Some(err) = stderr { + let app_clone = app.clone(); + threads.push(std::thread::spawn(move || { + let mut reader = std::io::BufReader::new(err); + let mut buf = Vec::new(); + loop { + buf.clear(); + match reader.read_until(b'\n', &mut buf) { + Ok(0) => break, + Ok(_) => { + let text = String::from_utf8_lossy(trim_line_endings(&buf)).into_owned(); + warn!("[install][stderr] {}", text); + let _ = app_clone.emit(event_mode.progress_event(), &text); + } + Err(e) => { + warn!("[install] Error reading stderr: {}", e); + break; + } + } + } + })); + } + + threads +} + +// ── Wait & Finalize ── + +/// Waits for the install process to exit. Returns (exit_status, was_intentional_stop). +/// Times out after 2 hours to prevent infinite loops if the child hangs. +fn wait_for_exit(state: &InstallState) -> Result<(ExitStatus, bool), String> { + const MAX_WAIT_ITERATIONS: u32 = 72_000; // 2h at 100ms intervals + for _ in 0..MAX_WAIT_ITERATIONS { + let mut install = state.lock().map_err(|e| e.to_string())?; + let intentional = install.intentional_stop; + + match install.child.as_mut() { + Some(child) => match child.try_wait() { + Ok(Some(status)) => { + install.child = None; + return Ok((status, intentional)); + } + Ok(None) => {} + Err(e) => { + install.child = None; + return Err(format!("Error waiting for installer: {}", e)); + } + }, + None if intentional => return Err("Installation stopped.".to_string()), + None => return Err("Installer process disappeared unexpectedly.".to_string()), + } + + drop(install); + std::thread::sleep(std::time::Duration::from_millis(100)); + } + // Timed out — kill and report + let _ = stop_install(state); + Err("Installation timed out after 2 hours".to_string()) +} + +// ── Public API ── + +/// Run the install script. Returns Ok(()) on success. +/// Returns Err("NEEDS_ELEVATION") if system packages need elevated install (Linux only). +/// Returns Err(message) on other failures. +pub fn run_install(app: AppHandle, state: InstallState) -> Result<(), String> { + run_install_with_event_mode(app, state, InstallEventMode::Full) +} + +pub(crate) fn run_install_for_repair(app: AppHandle, state: InstallState) -> Result<(), String> { + run_install_with_event_mode(app, state, InstallEventMode::Repair) +} + +fn run_install_with_event_mode( + app: AppHandle, + state: InstallState, + event_mode: InstallEventMode, +) -> Result<(), String> { + emit_mode_progress(&app, event_mode, "Starting installation..."); + + let (script, args) = resolve_install_script(&app)?; + emit_mode_progress( + &app, + event_mode, + &format!("Using script: {}", script.display()), + ); + + let (stdout, stderr) = spawn_script(&script, &args, &state)?; + let threads = stream_output(&app, &state, event_mode, stdout, stderr); + + // Wait for exit, join reader threads + let result = wait_for_exit(&state); + for handle in threads { + let _ = handle.join(); + } + + match result { + Ok((status, _)) if status.success() => { + if event_mode.emit_terminal_events() { + emit_complete(&app); + } + Ok(()) + } + Ok((status, _)) => { + let code = status.code().unwrap_or(-1); + if code == 2 { + // Script needs elevated package install — report to frontend + let packages = state + .lock() + .map(|i| i.needed_packages.clone()) + .unwrap_or_default(); + info!("[install] Needs elevation for packages: {:?}", packages); + let _ = app.emit(event_mode.needs_elevation_event(), &packages); + Err("NEEDS_ELEVATION".to_string()) + } else { + let msg = format!("Installer exited with code {}", code); + if event_mode.emit_terminal_events() { + emit_failed(&app, &msg); + } + Err(msg) + } + } + Err(msg) if msg == "Installation stopped." => { + info!("[install] Installation stopped intentionally"); + Err(msg) + } + Err(msg) => { + if event_mode.emit_terminal_events() { + emit_failed(&app, &msg); + } + Err(msg) + } + } +} + +/// Stop a running install process gracefully. +/// Unix: SIGTERM to process group -> wait up to 5s -> SIGKILL +/// Windows: hidden taskkill /T /F to terminate the installer tree +pub fn stop_install(state: &InstallState) -> Result<(), String> { + let mut child = { + let mut install = match state.lock() { + Ok(guard) => guard, + Err(poisoned) => { + warn!("Install state mutex poisoned, recovering for cleanup"); + poisoned.into_inner() + } + }; + install.intentional_stop = true; + install.child.take() + }; + + let Some(ref mut child) = child else { + return Ok(()); + }; + + let pid = child.id(); + info!("Stopping installer process group (pid {})", pid); + + // Try graceful SIGTERM first so pip/cmake can clean up temp files + #[cfg(unix)] + { + if pid > i32::MAX as u32 { + // PID too large for i32 negation, fall back to direct kill + warn!("PID {} exceeds i32 range, using direct kill", pid); + let _ = child.kill(); + let _ = child.wait(); + return Ok(()); + } + unsafe { + libc::kill(-(pid as i32), libc::SIGTERM); + } + // Wait up to 5s for graceful exit + for _ in 0..50 { + match child.try_wait() { + Ok(Some(status)) => { + info!("Installer exited gracefully with status: {:?}", status); + return Ok(()); + } + Ok(None) => std::thread::sleep(std::time::Duration::from_millis(100)), + Err(_) => break, + } + } + warn!("Installer did not exit gracefully, force killing"); + } + + #[cfg(windows)] + { + crate::process::force_kill_process_tree(pid, child, "Installer"); + return Ok(()); + } + + #[cfg(unix)] + { + // Force kill (SIGKILL on Unix) + let _ = child.kill(); + let _ = child.wait(); + info!("Installer process group force stopped"); + Ok(()) + } +} + +/// Install system packages with elevated permissions (Linux only). +/// Uses `elevated-command` crate for native auth dialog. +#[cfg(target_os = "linux")] +pub fn install_system_packages(packages: &[String]) -> Result<(), String> { + use regex::Regex; + use std::path::Path; + use std::process::Command as StdCommand; + + // Validate package names to prevent injection via elevated command. + let valid_pkg = Regex::new(r"^[a-zA-Z0-9][a-zA-Z0-9.+\-]*$").unwrap(); + for pkg in packages { + if !valid_pkg.is_match(pkg) { + return Err(format!("Invalid package name: {}", pkg)); + } + } + + // AppImage bundles run on non-Debian distros too. Pick the first package + // manager we find. Names in `packages` are Debian-style; callers that want + // cross-distro support should translate before invoking. + let (program, base_args): (&str, &[&str]) = if Path::new("/usr/bin/apt-get").exists() { + ("apt-get", &["install", "-y"]) + } else if Path::new("/usr/bin/dnf").exists() { + ("dnf", &["install", "-y"]) + } else if Path::new("/usr/bin/zypper").exists() { + ("zypper", &["install", "-y"]) + } else if Path::new("/usr/bin/pacman").exists() { + ("pacman", &["-S", "--noconfirm"]) + } else { + return Err( + "No supported system package manager found (apt-get, dnf, zypper, pacman)".to_string(), + ); + }; + + info!( + "[install] Elevated install of packages via {}: {}", + program, + packages.join(", ") + ); + + let mut cmd = StdCommand::new(program); + cmd.args(base_args).args(packages); + + let elevated = elevated_command::Command::new(cmd) + .output() + .map_err(|e| format!("Elevated install failed: {}", e))?; + + if !elevated.status.success() { + let stderr = String::from_utf8_lossy(&elevated.stderr); + return Err(format!("Package installation failed: {}", stderr)); + } + + info!("[install] Elevated package install succeeded"); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn repair_install_mode_uses_repair_elevation_event() { + assert_eq!( + InstallEventMode::Full.needs_elevation_event(), + "install-needs-elevation" + ); + assert_eq!( + InstallEventMode::Repair.needs_elevation_event(), + "repair-needs-elevation" + ); + assert!(!InstallEventMode::Repair.emit_terminal_events()); + } +} diff --git a/studio/src-tauri/src/main.rs b/studio/src-tauri/src/main.rs new file mode 100644 index 0000000000..f7d49cce60 --- /dev/null +++ b/studio/src-tauri/src/main.rs @@ -0,0 +1,189 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +mod commands; +mod desktop_auth; +mod install; +mod preflight; +mod process; +mod update; +mod windows_job; + +use log::info; +use process::new_backend_state; +use simplelog::{ + CombinedLogger, Config, LevelFilter, SharedLogger, TermLogger, TerminalMode, WriteLogger, +}; +use std::fs; +use tauri::menu::{MenuBuilder, MenuItemBuilder}; +use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; +use tauri::{Emitter, Manager}; + +fn setup_logging() { + let mut loggers: Vec> = vec![]; + + // Always log to stderr for development + loggers.push(TermLogger::new( + LevelFilter::Info, + Config::default(), + TerminalMode::Stderr, + simplelog::ColorChoice::Auto, + )); + + // Try to set up file logging to ~/.unsloth/studio/tauri.log + if let Some(home) = dirs::home_dir() { + let log_dir = home.join(".unsloth").join("studio"); + if fs::create_dir_all(&log_dir).is_ok() { + let log_path = log_dir.join("tauri.log"); + let rotated_path = log_dir.join("tauri.log.1"); + let max_log_bytes = 5 * 1024 * 1024; + if fs::metadata(&log_path) + .map(|meta| meta.len() >= max_log_bytes) + .unwrap_or(false) + { + let _ = fs::remove_file(&rotated_path); + let _ = fs::rename(&log_path, &rotated_path); + } + if let Ok(file) = fs::OpenOptions::new() + .create(true) + .append(true) + .open(&log_path) + { + loggers.push(WriteLogger::new(LevelFilter::Info, Config::default(), file)); + } + } + } + + if !loggers.is_empty() { + let _ = CombinedLogger::init(loggers); + } +} + +fn setup_tray(app: &tauri::App) -> Result<(), Box> { + let open = MenuItemBuilder::with_id("open", "Open Studio").build(app)?; + let toggle = MenuItemBuilder::with_id("toggle", "Start/Stop Server").build(app)?; + let quit = MenuItemBuilder::with_id("quit", "Quit").build(app)?; + let menu = MenuBuilder::new(app) + .items(&[&open, &toggle, &quit]) + .build()?; + + TrayIconBuilder::new() + .menu(&menu) + .tooltip("Unsloth Studio (Desktop)") + .icon(app.default_window_icon().unwrap().clone()) + .on_menu_event(move |app, event| match event.id().as_ref() { + "open" => { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } + } + "toggle" => { + let _ = app.emit("tray-toggle-server", ()); + } + "quit" => { + let install_state = app.state::(); + let _ = crate::install::stop_install(&install_state); + let update_state = app.state::(); + let _ = crate::update::stop_update(&update_state); + // Detach the 5s graceful-wait so the tray click does not + // block the Tauri main loop. Exit runs the RunEvent::Exit + // safety net which also calls stop_backend synchronously. + let shutdown = app.state::().inner().clone(); + let backend_state = app.state::().inner().clone(); + crate::process::stop_backend_detached(backend_state, shutdown); + app.exit(0); + } + _ => {} + }) + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + .. + } = event + { + if let Some(window) = tray.app_handle().get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } + } + }) + .build(app)?; + + Ok(()) +} + +fn main() { + // Fix PATH for GUI apps (macOS .app bundles, Linux AppImage, Windows) + // GUI apps don't inherit shell dotfile PATH — this spawns the user's + // login shell to source .zshrc/.bashrc/.profile and sets PATH properly. + let _ = fix_path_env::fix(); + + setup_logging(); + info!("Unsloth Studio desktop app starting"); + windows_job::initialize(); + + tauri::Builder::default() + .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { + if let Some(window) = app.get_webview_window("main") { + let _ = window.show(); + let _ = window.set_focus(); + } + })) + .plugin(tauri_plugin_process::init()) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_updater::Builder::new().build()) + .manage(install::new_install_state()) + .manage(new_backend_state()) + .manage(process::new_shutdown_flag()) + .manage(update::new_update_state()) + .invoke_handler(tauri::generate_handler![ + commands::check_install_status, + commands::desktop_preflight, + commands::start_install, + commands::start_server, + commands::start_managed_server, + commands::stop_server, + commands::check_health, + commands::get_server_logs, + commands::open_logs_dir, + commands::start_backend_update, + commands::start_managed_repair, + commands::install_system_packages, + desktop_auth::desktop_auth, + ]) + .setup(|app| { + setup_tray(app)?; + Ok(()) + }) + .on_window_event(|window, event| { + if let tauri::WindowEvent::CloseRequested { api, .. } = event { + // Hide window instead of closing — this is a tray app. + // Processes keep running so the backend stays available. + // Full cleanup happens via: + // - Tray "Quit" menu item (explicit user action) + // - RunEvent::Exit handler (OS shutdown, SIGTERM, etc.) + let _ = window.hide(); + api.prevent_close(); + } + }) + .build(tauri::generate_context!()) + .expect("error while building tauri application") + .run(|app, event| { + if let tauri::RunEvent::Exit = event { + // Cleanup on ALL exit paths — safety net for non-tray exits + if let Some(install_state) = app.try_state::() { + let _ = install::stop_install(&install_state); + } + if let Some(update_state) = app.try_state::() { + let _ = update::stop_update(&update_state); + } + if let Some(backend_state) = app.try_state::() { + let shutdown = app + .try_state::() + .expect("ShutdownFlag must be managed"); + let _ = process::stop_backend(&backend_state, &shutdown); + } + } + }); +} diff --git a/studio/src-tauri/src/preflight.rs b/studio/src-tauri/src/preflight.rs new file mode 100644 index 0000000000..d3df06d057 --- /dev/null +++ b/studio/src-tauri/src/preflight.rs @@ -0,0 +1,831 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::process::Stdio; +use std::time::Duration; +use tokio::io::AsyncReadExt; +use tokio::process::Command; + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DesktopPreflightDisposition { + NotInstalled, + ManagedReady, + ManagedStale, + AttachedReady, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DesktopPreflightResult { + pub disposition: DesktopPreflightDisposition, + pub reason: Option, + pub port: Option, + pub can_auto_repair: bool, + pub managed_bin: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum ManagedProbe { + Missing, + Ready { bin: PathBuf }, + Stale { bin: PathBuf, reason: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum BackendProbe { + Missing, + Ready { port: u16 }, + Old { port: u16, reason: String }, +} + +#[derive(Debug, Deserialize)] +struct DesktopCapability { + desktop_protocol_version: Option, + supports_api_only: Option, + supports_provision_desktop_auth: Option, + desktop_auth_stale_reason: Option, +} + +#[derive(Debug)] +struct BackendHealth { + desktop_protocol_version: Option, + supports_desktop_auth: Option, + stale_reason: Option, +} + +fn release_auto_repair() -> bool { + !cfg!(debug_assertions) +} + +fn choose_preflight(managed: ManagedProbe, backend: BackendProbe) -> DesktopPreflightResult { + match (backend, managed) { + (BackendProbe::Ready { port }, ManagedProbe::Ready { bin }) => DesktopPreflightResult { + disposition: DesktopPreflightDisposition::AttachedReady, + reason: None, + port: Some(port), + can_auto_repair: false, + managed_bin: Some(bin), + }, + (_, managed) => match managed { + ManagedProbe::Ready { bin } => DesktopPreflightResult { + disposition: DesktopPreflightDisposition::ManagedReady, + reason: None, + port: None, + can_auto_repair: false, + managed_bin: Some(bin), + }, + ManagedProbe::Stale { bin, reason } => DesktopPreflightResult { + disposition: DesktopPreflightDisposition::ManagedStale, + reason: Some(reason), + port: None, + can_auto_repair: release_auto_repair(), + managed_bin: Some(bin), + }, + ManagedProbe::Missing => DesktopPreflightResult { + disposition: DesktopPreflightDisposition::NotInstalled, + reason: None, + port: None, + can_auto_repair: false, + managed_bin: None, + }, + }, + } +} + +async fn run_cli_probe(bin: &std::path::Path, args: &[&str]) -> bool { + let mut cmd = Command::new(bin); + cmd.args(args).stdout(Stdio::null()).stderr(Stdio::null()); + + #[cfg(target_os = "linux")] + if std::env::var_os("APPIMAGE").is_some() { + cmd.env_remove("LD_LIBRARY_PATH"); + cmd.env_remove("PYTHONHOME"); + cmd.env_remove("PYTHONPATH"); + } + + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + cmd.creation_flags(crate::process::CREATE_NO_WINDOW); + } + + let Ok(mut child) = cmd.spawn() else { + return false; + }; + + match tokio::time::timeout(Duration::from_secs(10), child.wait()).await { + Ok(Ok(status)) => status.success(), + _ => { + let _ = child.kill().await; + let _ = child.wait().await; + false + } + } +} + +async fn probe_cli_capability(bin: &std::path::Path) -> Option { + let mut cmd = Command::new(bin); + cmd.args(["studio", "desktop-capabilities", "--json"]) + .stdout(Stdio::piped()) + .stderr(Stdio::null()); + + #[cfg(target_os = "linux")] + if std::env::var_os("APPIMAGE").is_some() { + cmd.env_remove("LD_LIBRARY_PATH"); + cmd.env_remove("PYTHONHOME"); + cmd.env_remove("PYTHONPATH"); + } + + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + cmd.creation_flags(crate::process::CREATE_NO_WINDOW); + } + + let Ok(mut child) = cmd.spawn() else { + return None; + }; + let Some(mut stdout) = child.stdout.take() else { + return None; + }; + + match tokio::time::timeout(Duration::from_secs(10), child.wait()).await { + Ok(Ok(status)) if status.success() => {} + Err(_) => { + let _ = child.kill().await; + let _ = child.wait().await; + return None; + } + _ => return None, + } + + let mut output = Vec::new(); + if stdout.read_to_end(&mut output).await.is_err() { + return None; + } + + serde_json::from_slice::(&output).ok() +} + +fn desktop_capability_ready(capability: &DesktopCapability) -> bool { + capability.desktop_protocol_version == Some(1) + && capability.supports_api_only == Some(true) + && capability.supports_provision_desktop_auth == Some(true) +} + +fn desktop_capability_stale_reason(capability: &DesktopCapability) -> String { + capability + .desktop_auth_stale_reason + .clone() + .unwrap_or_else(|| "desktop_capability_incompatible".to_string()) +} + +async fn probe_managed_bin(bin: PathBuf) -> ManagedProbe { + if !run_cli_probe(&bin, &["-h"]).await { + return ManagedProbe::Stale { + bin, + reason: "cli_unusable".to_string(), + }; + } + + let capability = probe_cli_capability(&bin).await; + if let Some(capability) = capability { + if desktop_capability_ready(&capability) { + return ManagedProbe::Ready { bin }; + } + return ManagedProbe::Stale { + bin, + reason: desktop_capability_stale_reason(&capability), + }; + } + + ManagedProbe::Stale { + bin, + reason: "desktop_capability_probe_failed".to_string(), + } +} + +async fn probe_managed_install() -> ManagedProbe { + match crate::process::find_unsloth_binary() { + Some(bin) => probe_managed_bin(bin).await, + None => ManagedProbe::Missing, + } +} + +pub async fn managed_install_ready() -> bool { + matches!(probe_managed_install().await, ManagedProbe::Ready { .. }) +} + +async fn backend_health(client: &reqwest::Client, port: u16) -> Option { + let url = format!("http://127.0.0.1:{port}/api/health"); + let response = client.get(url).send().await.ok()?; + if !response.status().is_success() { + return None; + } + let json = response.json::().await.ok()?; + let healthy = json + .get("status") + .and_then(|v| v.as_str()) + .map(|s| s == "healthy") + .unwrap_or(false); + let service = json + .get("service") + .and_then(|v| v.as_str()) + .map(|s| s == "Unsloth UI Backend") + .unwrap_or(false); + if !healthy || !service { + return None; + } + + let desktop_protocol_version = json + .get("desktop_protocol_version") + .and_then(|v| v.as_u64()) + .and_then(|v| u16::try_from(v).ok()); + let supports_desktop_auth = json.get("supports_desktop_auth").and_then(|v| v.as_bool()); + if desktop_protocol_version.is_none() && supports_desktop_auth.is_none() { + return None; + } + let stale_reason = match supports_desktop_auth { + Some(false) => json + .get("desktop_auth_stale_reason") + .and_then(|v| v.as_str()) + .map(ToOwned::to_owned), + _ => None, + }; + Some(BackendHealth { + desktop_protocol_version, + supports_desktop_auth, + stale_reason, + }) +} + +fn backend_capability_stale_reason(health: &BackendHealth) -> Option { + if health.desktop_protocol_version != Some(1) { + return health + .stale_reason + .clone() + .or_else(|| Some("desktop_protocol_incompatible".to_string())); + } + if health.supports_desktop_auth != Some(true) { + return health + .stale_reason + .clone() + .or_else(|| Some("desktop_auth_unsupported".to_string())); + } + None +} + +#[derive(Serialize)] +struct DesktopLoginProbe<'a> { + secret: &'a str, +} + +async fn backend_desktop_auth_status( + client: &reqwest::Client, + port: u16, + health: &BackendHealth, +) -> BackendProbe { + if let Some(reason) = backend_capability_stale_reason(health) { + return BackendProbe::Old { port, reason }; + } + + let url = format!("http://127.0.0.1:{port}/api/auth/desktop-login"); + let response = client + .post(url) + .json(&DesktopLoginProbe { + secret: "desktop-preflight-invalid-secret", + }) + .send() + .await; + + let Ok(response) = response else { + return BackendProbe::Old { + port, + reason: backend_capability_stale_reason(health) + .unwrap_or_else(|| "desktop_login_probe_failed".to_string()), + }; + }; + + match response.status() { + reqwest::StatusCode::UNAUTHORIZED => BackendProbe::Ready { port }, + reqwest::StatusCode::NOT_FOUND => BackendProbe::Old { + port, + reason: "desktop_login_not_found".to_string(), + }, + _ => BackendProbe::Old { + port, + reason: backend_capability_stale_reason(health) + .unwrap_or_else(|| "desktop_login_probe_failed".to_string()), + }, + } +} + +async fn probe_existing_backends() -> BackendProbe { + let client = match reqwest::Client::builder() + .timeout(Duration::from_secs(2)) + .build() + { + Ok(client) => client, + Err(_) => return BackendProbe::Missing, + }; + + // Fan out health probes concurrently. The desktop-auth probe is still + // sequential per candidate because it has auth-log side effects. + let ports: Vec = (8888u16..=8908).collect(); + let mut health_futs = Vec::with_capacity(ports.len()); + for port in ports { + // why: reqwest::Client is internally Arc-wrapped; clone is a refcount bump + // (documented cheap). tokio::spawn needs 'static, so each task owns its own clone. + let c = client.clone(); + health_futs.push(tokio::spawn(async move { + backend_health(&c, port).await.map(|h| (port, h)) + })); + } + + let mut candidates: Vec<(u16, BackendHealth)> = Vec::new(); + for fut in health_futs { + if let Ok(Some(pair)) = fut.await { + candidates.push(pair); + } + } + + let mut first_old = None; + for (port, health) in candidates { + match backend_desktop_auth_status(&client, port, &health).await { + ready @ BackendProbe::Ready { .. } => return ready, + old @ BackendProbe::Old { .. } if first_old.is_none() => first_old = Some(old), + _ => {} + } + } + + first_old.unwrap_or(BackendProbe::Missing) +} + +pub async fn desktop_preflight_result() -> DesktopPreflightResult { + let (managed, backend) = tokio::join!(probe_managed_install(), probe_existing_backends()); + choose_preflight(managed, backend) +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::net::TcpListener; + + #[test] + fn compatible_backend_does_not_win_over_stale_managed_install() { + let result = choose_preflight( + ManagedProbe::Stale { + bin: PathBuf::from("/managed/unsloth"), + reason: "old cli".to_string(), + }, + BackendProbe::Ready { port: 8000 }, + ); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::ManagedStale + ); + assert_eq!(result.port, None); + assert_eq!(result.reason, Some("old cli".to_string())); + assert_eq!(result.can_auto_repair, release_auto_repair()); + assert_eq!(result.managed_bin, Some(PathBuf::from("/managed/unsloth"))); + } + + #[test] + fn compatible_backend_wins_over_ready_managed_install() { + let result = choose_preflight( + ManagedProbe::Ready { + bin: PathBuf::from("/managed/unsloth"), + }, + BackendProbe::Ready { port: 8000 }, + ); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::AttachedReady + ); + assert_eq!(result.port, Some(8000)); + assert_eq!(result.managed_bin, Some(PathBuf::from("/managed/unsloth"))); + assert!(!result.can_auto_repair); + } + + #[test] + fn compatible_backend_does_not_win_over_missing_managed_install() { + let result = choose_preflight(ManagedProbe::Missing, BackendProbe::Ready { port: 8000 }); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::NotInstalled + ); + assert_eq!(result.port, None); + assert_eq!(result.managed_bin, None); + assert!(!result.can_auto_repair); + } + + #[test] + fn old_backend_falls_back_to_ready_managed_install() { + let result = choose_preflight( + ManagedProbe::Ready { + bin: PathBuf::from("/managed/unsloth"), + }, + BackendProbe::Old { + port: 8001, + reason: "missing endpoint".to_string(), + }, + ); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::ManagedReady + ); + assert_eq!(result.reason, None); + assert_eq!(result.port, None); + assert_eq!(result.managed_bin, Some(PathBuf::from("/managed/unsloth"))); + assert!(!result.can_auto_repair); + } + + #[test] + fn old_backend_falls_back_to_stale_managed_install() { + let result = choose_preflight( + ManagedProbe::Stale { + bin: PathBuf::from("/managed/unsloth"), + reason: "old cli".to_string(), + }, + BackendProbe::Old { + port: 8001, + reason: "missing endpoint".to_string(), + }, + ); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::ManagedStale + ); + assert_eq!(result.reason, Some("old cli".to_string())); + assert_eq!(result.port, None); + assert_eq!(result.can_auto_repair, release_auto_repair()); + assert_eq!(result.managed_bin, Some(PathBuf::from("/managed/unsloth"))); + } + + #[test] + fn managed_ready_when_no_backend() { + let result = choose_preflight( + ManagedProbe::Ready { + bin: PathBuf::from("/managed/unsloth"), + }, + BackendProbe::Missing, + ); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::ManagedReady + ); + assert_eq!(result.managed_bin, Some(PathBuf::from("/managed/unsloth"))); + assert!(!result.can_auto_repair); + } + + #[test] + fn managed_stale_when_no_backend() { + let result = choose_preflight( + ManagedProbe::Stale { + bin: PathBuf::from("/managed/unsloth"), + reason: "old cli".to_string(), + }, + BackendProbe::Missing, + ); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::ManagedStale + ); + assert_eq!(result.reason, Some("old cli".to_string())); + assert_eq!(result.can_auto_repair, release_auto_repair()); + } + + #[test] + fn not_installed_when_no_backend_no_managed_binary() { + let result = choose_preflight(ManagedProbe::Missing, BackendProbe::Missing); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::NotInstalled + ); + assert_eq!(result.managed_bin, None); + assert!(!result.can_auto_repair); + } + + #[test] + fn old_backend_with_no_managed_install_uses_install_flow() { + let result = choose_preflight( + ManagedProbe::Missing, + BackendProbe::Old { + port: 8002, + reason: "old version".to_string(), + }, + ); + + assert_eq!( + result.disposition, + DesktopPreflightDisposition::NotInstalled + ); + assert_eq!(result.reason, None); + assert_eq!(result.port, None); + assert!(!result.can_auto_repair); + } + + #[cfg(unix)] + struct FakeCli { + bin: PathBuf, + dir: PathBuf, + } + + #[cfg(unix)] + impl Drop for FakeCli { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.dir); + } + } + + #[cfg(unix)] + fn fake_cli(test_name: &str, script: &str) -> FakeCli { + use std::fs; + use std::os::unix::fs::PermissionsExt; + use std::time::{SystemTime, UNIX_EPOCH}; + + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let dir = std::env::temp_dir().join(format!( + "unsloth-preflight-{test_name}-{}-{nanos}", + std::process::id() + )); + fs::create_dir_all(&dir).unwrap(); + let bin = dir.join("unsloth"); + fs::write(&bin, script).unwrap(); + let mut perms = fs::metadata(&bin).unwrap().permissions(); + perms.set_mode(0o755); + fs::set_permissions(&bin, perms).unwrap(); + FakeCli { bin, dir } + } + + #[cfg(unix)] + #[tokio::test] + async fn managed_cli_stale_when_desktop_capabilities_missing() { + let fake = fake_cli( + "cap-missing", + r#"#!/bin/sh +if [ "$1" = "-h" ]; then exit 0; fi +if [ "$1" = "studio" ] && [ "$2" = "provision-desktop-auth" ] && [ "$3" = "--help" ]; then exit 0; fi +exit 1 +"#, + ); + let bin = fake.bin.clone(); + + assert!(matches!( + probe_managed_bin(bin.clone()).await, + ManagedProbe::Stale { bin: actual_bin, .. } if actual_bin == bin + )); + } + + #[cfg(unix)] + #[tokio::test] + async fn managed_cli_stale_when_desktop_capabilities_command_missing() { + let fake = fake_cli( + "missing-helper", + r#"#!/bin/sh +if [ "$1" = "-h" ]; then exit 0; fi +exit 1 +"#, + ); + let bin = fake.bin.clone(); + + assert!(matches!( + probe_managed_bin(bin.clone()).await, + ManagedProbe::Stale { bin: actual_bin, .. } if actual_bin == bin + )); + } + + #[cfg(unix)] + #[tokio::test] + async fn managed_cli_stale_when_help_broken() { + let fake = fake_cli( + "broken-help", + r#"#!/bin/sh +exit 1 +"#, + ); + let bin = fake.bin.clone(); + + assert_eq!( + probe_managed_bin(bin.clone()).await, + ManagedProbe::Stale { + bin, + reason: "cli_unusable".to_string() + } + ); + } + + #[cfg(unix)] + #[tokio::test] + async fn managed_cli_ready_when_desktop_capabilities_compatible() { + let fake = fake_cli( + "cap-true-helper-missing", + r#"#!/bin/sh +if [ "$1" = "-h" ]; then exit 0; fi +if [ "$1" = "studio" ] && [ "$2" = "desktop-capabilities" ] && [ "$3" = "--json" ]; then + printf '{"desktop_protocol_version":1,"supports_api_only":true,"supports_provision_desktop_auth":true}' + exit 0 +fi +exit 1 +"#, + ); + let bin = fake.bin.clone(); + + assert_eq!( + probe_managed_bin(bin.clone()).await, + ManagedProbe::Ready { bin } + ); + } + + #[cfg(unix)] + #[tokio::test] + async fn capability_false_reason_used_when_legacy_helper_missing() { + let fake = fake_cli( + "cap-false-helper-missing", + r#"#!/bin/sh +if [ "$1" = "-h" ]; then exit 0; fi +if [ "$1" = "studio" ] && [ "$2" = "desktop-capabilities" ] && [ "$3" = "--json" ]; then + printf '{"desktop_protocol_version":1,"supports_api_only":true,"supports_provision_desktop_auth":false,"desktop_auth_stale_reason":"cap_false"}' + exit 0 +fi +exit 1 +"#, + ); + let bin = fake.bin.clone(); + + assert_eq!( + probe_managed_bin(bin.clone()).await, + ManagedProbe::Stale { + bin, + reason: "cap_false".to_string() + } + ); + } + + #[cfg(unix)] + #[tokio::test] + async fn capability_false_overrides_working_legacy_helper() { + let fake = fake_cli( + "cap-false-helper-ready", + r#"#!/bin/sh +if [ "$1" = "-h" ]; then exit 0; fi +if [ "$1" = "studio" ] && [ "$2" = "desktop-capabilities" ] && [ "$3" = "--json" ]; then + printf '{"desktop_protocol_version":1,"supports_api_only":true,"supports_provision_desktop_auth":false,"desktop_auth_stale_reason":"cap_false"}' + exit 0 +fi +if [ "$1" = "studio" ] && [ "$2" = "provision-desktop-auth" ] && [ "$3" = "--help" ]; then exit 0; fi +exit 1 +"#, + ); + let bin = fake.bin.clone(); + + assert_eq!( + probe_managed_bin(bin.clone()).await, + ManagedProbe::Stale { + bin, + reason: "cap_false".to_string() + } + ); + } + + async fn backend_server(health_body: &'static str, route_status: &'static str) -> u16 { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let port = listener.local_addr().unwrap().port(); + + tokio::spawn(async move { + for _ in 0..2 { + let (mut stream, _) = listener.accept().await.unwrap(); + let mut buffer = [0; 2048]; + let n = stream.read(&mut buffer).await.unwrap(); + let request = String::from_utf8_lossy(&buffer[..n]); + let (status, body) = if request.starts_with("GET /api/health ") { + ("200 OK", health_body) + } else if request.starts_with("POST /api/auth/desktop-login ") { + (route_status, "") + } else { + ("404 Not Found", "") + }; + let response = format!( + "HTTP/1.1 {status}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ); + stream.write_all(response.as_bytes()).await.unwrap(); + } + }); + + port + } + + async fn probe_test_backend( + health_body: &'static str, + route_status: &'static str, + ) -> BackendProbe { + let port = backend_server(health_body, route_status).await; + let client = reqwest::Client::new(); + let health = backend_health(&client, port).await.unwrap(); + backend_desktop_auth_status(&client, port, &health).await + } + + #[tokio::test] + async fn backend_health_without_desktop_capability_fields_is_not_compatible() { + let port = backend_server( + r#"{"status":"healthy","service":"Unsloth UI Backend"}"#, + "401 Unauthorized", + ) + .await; + let client = reqwest::Client::new(); + + assert!(backend_health(&client, port).await.is_none()); + } + + #[tokio::test] + async fn backend_with_auth_support_but_missing_protocol_is_old() { + let probe = probe_test_backend( + r#"{"status":"healthy","service":"Unsloth UI Backend","supports_desktop_auth":true}"#, + "401 Unauthorized", + ) + .await; + + assert!(matches!(probe, BackendProbe::Old { .. })); + } + + #[tokio::test] + async fn backend_with_auth_support_but_unsupported_protocol_is_old() { + let probe = probe_test_backend( + r#"{"status":"healthy","service":"Unsloth UI Backend","desktop_protocol_version":2,"supports_desktop_auth":true}"#, + "401 Unauthorized", + ) + .await; + + assert!(matches!(probe, BackendProbe::Old { .. })); + } + + #[tokio::test] + async fn backend_health_with_desktop_capability_fields_and_401_is_ready() { + let probe = probe_test_backend( + r#"{"status":"healthy","service":"Unsloth UI Backend","desktop_protocol_version":1,"supports_desktop_auth":true}"#, + "401 Unauthorized", + ) + .await; + + assert!(matches!(probe, BackendProbe::Ready { .. })); + } + + #[tokio::test] + async fn backend_route_404_is_old() { + let probe = probe_test_backend( + r#"{"status":"healthy","service":"Unsloth UI Backend","desktop_protocol_version":1,"supports_desktop_auth":true}"#, + "404 Not Found", + ) + .await; + + assert!(matches!( + probe, + BackendProbe::Old { + reason, + .. + } if reason == "desktop_login_not_found" + )); + } + + #[tokio::test] + async fn backend_route_500_is_old() { + let probe = probe_test_backend( + r#"{"status":"healthy","service":"Unsloth UI Backend","desktop_protocol_version":1,"supports_desktop_auth":true}"#, + "500 Internal Server Error", + ) + .await; + + assert!(matches!(probe, BackendProbe::Old { .. })); + } + + #[tokio::test] + async fn backend_capability_false_is_old_even_when_route_401() { + let port = backend_server( + r#"{"status":"healthy","service":"Unsloth UI Backend","desktop_protocol_version":1,"supports_desktop_auth":false,"desktop_auth_stale_reason":"cap_false"}"#, + "401 Unauthorized", + ) + .await; + let client = reqwest::Client::new(); + let health = backend_health(&client, port).await.unwrap(); + + assert!(matches!( + backend_desktop_auth_status(&client, port, &health).await, + BackendProbe::Old { + reason, + .. + } if reason == "cap_false" + )); + } +} diff --git a/studio/src-tauri/src/process.rs b/studio/src-tauri/src/process.rs new file mode 100644 index 0000000000..beba8c1208 --- /dev/null +++ b/studio/src-tauri/src/process.rs @@ -0,0 +1,537 @@ +use log::{error, info, warn}; +use process_wrap::std::*; +use regex::Regex; +use std::collections::VecDeque; +use std::io::BufRead; +use std::process::{Command, Stdio}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use tauri::{AppHandle, Emitter}; + +const MAX_LOG_LINES: usize = 1000; + +pub struct BackendProcess { + pub child: Option>, + pub port: Option, + pub logs: VecDeque, + pub intentional_stop: bool, +} + +impl Default for BackendProcess { + fn default() -> Self { + Self { + child: None, + port: None, + logs: VecDeque::with_capacity(MAX_LOG_LINES), + intentional_stop: false, + } + } +} + +pub type BackendState = Arc>; +pub type ShutdownFlag = Arc; + +pub fn new_backend_state() -> BackendState { + Arc::new(Mutex::new(BackendProcess::default())) +} + +pub fn new_shutdown_flag() -> ShutdownFlag { + Arc::new(AtomicBool::new(false)) +} + +pub(crate) fn trim_line_endings(bytes: &[u8]) -> &[u8] { + let mut end = bytes.len(); + while end > 0 && matches!(bytes[end - 1], b'\n' | b'\r') { + end -= 1; + } + &bytes[..end] +} + +/// Windows `CREATE_NO_WINDOW` flag — suppresses console windows for child processes. +#[cfg(windows)] +pub(crate) const CREATE_NO_WINDOW: u32 = 0x08000000; + +/// Force-kill a Windows process tree via hidden `taskkill /T /F`, falling +/// back to `child.kill()` if taskkill itself fails. Reaps the child afterward. +#[cfg(windows)] +pub(crate) fn force_kill_process_tree( + pid: u32, + child: &mut Box, + label: &str, +) { + use std::os::windows::process::CommandExt; + + let taskkill_status = Command::new("taskkill.exe") + .creation_flags(CREATE_NO_WINDOW) + .args(["/PID", &pid.to_string(), "/T", "/F"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + + match taskkill_status { + Ok(status) if status.success() => {} + Ok(status) => { + warn!( + "taskkill returned non-zero status for {} pid {}: {}", + label, pid, status + ); + let _ = child.kill(); + } + Err(e) => { + warn!("taskkill failed for {} pid {}: {}", label, pid, e); + let _ = child.kill(); + } + } + + let _ = child.wait(); + info!("{} process tree force stopped", label); +} + +/// Returns the path to the unsloth binary inside the managed venv, if it exists. +/// Checks the new layout (~/.unsloth/studio/unsloth_studio/) first, +/// then falls back to the old layout (~/.unsloth/studio/.venv/) for compat. +fn find_unsloth_binary_in_studio_dir(studio: &std::path::Path) -> Option { + // New layout (upstream scripts >= March 2026) + let new_base = studio.join("unsloth_studio"); + // Old layout (bundled scripts, older upstream) + let old_base = studio.join(".venv"); + + for base in [new_base, old_base] { + #[cfg(unix)] + let bin = base.join("bin").join("unsloth"); + #[cfg(windows)] + let bin = base.join("Scripts").join("unsloth.exe"); + + if bin.exists() { + return Some(bin); + } + } + + None +} + +pub fn find_unsloth_binary() -> Option { + let home = dirs::home_dir()?; + let studio = home.join(".unsloth").join("studio"); + + find_unsloth_binary_in_studio_dir(&studio) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::path::PathBuf; + use std::time::{SystemTime, UNIX_EPOCH}; + + fn temp_studio_dir(test_name: &str) -> PathBuf { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(); + let dir = std::env::temp_dir().join(format!( + "unsloth-{test_name}-{}-{nanos}", + std::process::id() + )); + fs::create_dir_all(&dir).unwrap(); + dir + } + + #[test] + fn finds_new_layout_before_legacy_layout() { + let temp = temp_studio_dir("new-before-legacy"); + + #[cfg(unix)] + let new_bin = temp.join("unsloth_studio/bin/unsloth"); + #[cfg(unix)] + let old_bin = temp.join(".venv/bin/unsloth"); + #[cfg(windows)] + let new_bin = temp.join("unsloth_studio/Scripts/unsloth.exe"); + #[cfg(windows)] + let old_bin = temp.join(".venv/Scripts/unsloth.exe"); + + fs::create_dir_all(new_bin.parent().unwrap()).unwrap(); + fs::create_dir_all(old_bin.parent().unwrap()).unwrap(); + fs::write(&new_bin, "").unwrap(); + fs::write(&old_bin, "").unwrap(); + + assert_eq!(find_unsloth_binary_in_studio_dir(&temp), Some(new_bin)); + fs::remove_dir_all(temp).unwrap(); + } + + #[test] + fn finds_legacy_layout_when_new_missing() { + let temp = temp_studio_dir("legacy"); + + #[cfg(unix)] + let old_bin = temp.join(".venv/bin/unsloth"); + #[cfg(windows)] + let old_bin = temp.join(".venv/Scripts/unsloth.exe"); + + fs::create_dir_all(old_bin.parent().unwrap()).unwrap(); + fs::write(&old_bin, "").unwrap(); + + assert_eq!(find_unsloth_binary_in_studio_dir(&temp), Some(old_bin)); + fs::remove_dir_all(temp).unwrap(); + } + + #[test] + fn returns_none_when_no_managed_layout_exists() { + let temp = temp_studio_dir("none"); + + assert_eq!(find_unsloth_binary_in_studio_dir(&temp), None); + fs::remove_dir_all(temp).unwrap(); + } + + #[test] + fn backend_args_always_enable_api_only() { + assert_eq!( + backend_args(8888), + vec!["studio", "--api-only", "-H", "127.0.0.1", "-p", "8888"] + ); + } +} + +/// Find the unsloth binary, preferring the dev repo if available. +/// In dev mode (debug builds), checks for a local .venv in the repo first. +/// Falls back to find_unsloth_binary() which checks ~/.unsloth/studio/unsloth_studio/ +/// (new layout) then ~/.unsloth/studio/.venv/ (old layout). +pub(crate) fn resolve_backend_binary() -> Result { + // In dev mode, check for local repo venv first + #[cfg(debug_assertions)] + { + // CARGO_MANIFEST_DIR is set at compile time to studio/src-tauri/ + // Repo root is 2 levels up: studio/src-tauri -> studio -> repo_root + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let repo_root = std::path::Path::new(manifest_dir) + .parent() // studio/ + .and_then(|p| p.parent()); // repo_root/ + + if let Some(root) = repo_root { + #[cfg(unix)] + let dev_bin = root.join(".venv/bin/unsloth"); + #[cfg(windows)] + let dev_bin = root.join(".venv/Scripts/unsloth.exe"); + + if dev_bin.exists() { + info!("Dev mode: using local repo backend at {:?}", dev_bin); + return Ok(dev_bin.to_path_buf()); + } + } + info!("Dev mode: no local .venv found, falling back to installed backend"); + } + + find_unsloth_binary() + .ok_or_else(|| "Unsloth binary not found. Please install Unsloth Studio first.".to_string()) +} + +fn backend_args(port: u16) -> Vec { + [ + "studio", + "--api-only", + "-H", + "127.0.0.1", + "-p", + &port.to_string(), + ] + .into_iter() + .map(String::from) + .collect() +} + +/// Spawn the backend process and wire up stdout/stderr reader threads. +pub fn start_backend( + app: &AppHandle, + state: &BackendState, + port: u16, + shutdown: &ShutdownFlag, +) -> Result<(), String> { + let bin = resolve_backend_binary()?; + + shutdown.store(false, Ordering::SeqCst); + + // Reset state + { + let mut proc = state.lock().map_err(|e| e.to_string())?; + if proc.child.is_some() { + return Err("Backend is already running.".to_string()); + } + proc.port = None; + proc.logs.clear(); + proc.intentional_stop = false; + } + + let args = backend_args(port); + info!("Starting backend: {:?} {}", bin, args.join(" ")); + + let mut cmd = Command::new(&bin); + cmd.args(&args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + // AppImage sets LD_LIBRARY_PATH to its bundled libs, which breaks the spawned + // Python process (wrong libpython/libz → "No module named encodings"). + // Only clear when running inside an AppImage — native .deb/.rpm installs may + // need these env vars for custom CUDA or conda paths. + #[cfg(target_os = "linux")] + if std::env::var_os("APPIMAGE").is_some() { + cmd.env_remove("LD_LIBRARY_PATH"); + cmd.env_remove("PYTHONHOME"); + cmd.env_remove("PYTHONPATH"); + } + + // On Windows, launch the backend directly with hidden-window flags. + // The app process is assigned to a KILL_ON_JOB_CLOSE job in main.rs, so + // children inherit crash-safe cleanup without the buggy per-child JobObject wrapper. + #[cfg(windows)] + let mut child: Box = { + use std::os::windows::process::CommandExt; + + const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; + cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW); + let child = cmd + .spawn() + .map_err(|e| format!("Failed to spawn backend: {}", e))?; + Box::new(child) + }; + + #[cfg(unix)] + let mut child: Box = { + // Keep the backend tree in a process group on Unix for cleanup. + let mut wrap = CommandWrap::from(cmd); + wrap.wrap(ProcessGroup::leader()); + wrap.spawn() + .map_err(|e| format!("Failed to spawn backend: {}", e))? + }; + + let stdout = child.stdout().take(); + let stderr = child.stderr().take(); + + // Store child in state + { + let mut proc = state.lock().map_err(|e| e.to_string())?; + proc.child = Some(child); + } + + // Spawn stdout reader thread + if let Some(stdout) = stdout { + let app_handle = app.clone(); + let state_clone = Arc::clone(state); + std::thread::spawn(move || { + read_output_stream(stdout, &app_handle, &state_clone, false); + }); + } + + // Spawn stderr reader thread + if let Some(stderr) = stderr { + let app_handle = app.clone(); + let state_clone = Arc::clone(state); + std::thread::spawn(move || { + read_output_stream(stderr, &app_handle, &state_clone, true); + }); + } + + Ok(()) +} + +/// Read lines from a child process stream (stdout or stderr). +/// For stdout, parse TAURI_PORT=(\d+) to detect the actual port. +/// When stdout closes and the stop was not intentional, emit server-crashed. +fn read_output_stream( + stream: R, + app: &AppHandle, + state: &BackendState, + is_stderr: bool, +) { + let mut reader = std::io::BufReader::new(stream); + let port_re = Regex::new(r"TAURI_PORT=(\d+)").unwrap(); + let mut buf = Vec::new(); + + loop { + buf.clear(); + match reader.read_until(b'\n', &mut buf) { + Ok(0) => break, + Ok(_) => { + let text = String::from_utf8_lossy(trim_line_endings(&buf)).into_owned(); + let log_line = if is_stderr { + format!("[stderr] {}", text) + } else { + text.clone() + }; + + // Check for TAURI_PORT on stdout only + if !is_stderr { + if let Some(caps) = port_re.captures(&text) { + if let Some(port_str) = caps.get(1) { + if let Ok(port) = port_str.as_str().parse::() { + info!("Detected backend port: {}", port); + if let Ok(mut proc) = state.lock() { + proc.port = Some(port); + } + let _ = app.emit("server-port", port); + } + } + } + } + + // Buffer the log line + if let Ok(mut proc) = state.lock() { + if proc.logs.len() >= MAX_LOG_LINES { + proc.logs.pop_front(); + } + proc.logs.push_back(log_line.clone()); + } + + info!("[backend] {}", log_line); + + // Emit to frontend + let _ = app.emit("server-log", &log_line); + } + Err(e) => { + warn!( + "Error reading backend {}: {}", + if is_stderr { "stderr" } else { "stdout" }, + e + ); + break; + } + } + } + + // Stream closed. Only the stdout reader checks for crashes. + if !is_stderr { + if let Ok(mut proc) = state.lock() { + let intentional = proc.intentional_stop; + let exited = if let Some(ref mut child) = proc.child { + match child.try_wait() { + Ok(Some(status)) => { + info!("Backend stdout stream ended with status: {}", status); + true + } + Ok(None) => { + warn!("Backend stdout stream ended, but process is still running"); + false + } + Err(e) => { + warn!("Failed to query backend status after stdout closed: {}", e); + false + } + } + } else { + false + }; + + if exited { + proc.child = None; + if !intentional { + error!("Backend process stdout closed unexpectedly (crash detected)"); + let _ = app.emit("server-crashed", ()); + } + } + } + } +} + +/// Graceful shutdown of the backend process and its entire subprocess tree. +/// Unix: SIGTERM to process group -> wait up to 5s -> SIGKILL to group +/// Windows: CTRL_BREAK_EVENT -> wait up to 5s -> hidden taskkill /T /F +pub fn stop_backend(state: &BackendState, shutdown: &ShutdownFlag) -> Result<(), String> { + shutdown.store(true, Ordering::SeqCst); + + // Extract the child and mark intentional stop. + // We take the child OUT of the mutex so we don't hold the lock during the wait loop. + let mut child = { + let mut proc = match state.lock() { + Ok(guard) => guard, + Err(poisoned) => { + warn!("Backend state mutex poisoned, recovering for cleanup"); + poisoned.into_inner() + } + }; + proc.intentional_stop = true; + proc.child.take() + }; + + let Some(ref mut child) = child else { + return Ok(()); // Nothing running + }; + + let pid = child.id(); + info!("Stopping backend process group (pid {})", pid); + + // Send SIGTERM to the entire process group (negative PID = group signal). + // This gives Python and any workers a chance to shut down gracefully. + #[cfg(unix)] + { + if pid > i32::MAX as u32 { + // PID too large for i32 negation, fall back to direct kill + warn!("PID {} exceeds i32 range, using direct kill", pid); + let _ = child.kill(); + let _ = child.wait(); + return Ok(()); + } + unsafe { + libc::kill(-(pid as i32), libc::SIGTERM); + } + } + #[cfg(windows)] + { + unsafe { + windows_sys::Win32::System::Console::GenerateConsoleCtrlEvent( + windows_sys::Win32::System::Console::CTRL_BREAK_EVENT, + pid, + ); + } + } + + // Poll for up to 5 seconds (50 iterations * 100ms) + for _ in 0..50 { + match child.try_wait() { + Ok(Some(status)) => { + info!("Backend exited with status: {}", status); + return Ok(()); + } + Ok(None) => { + std::thread::sleep(std::time::Duration::from_millis(100)); + } + Err(e) => { + warn!("Error polling backend process: {}", e); + break; + } + } + } + + #[cfg(windows)] + { + warn!( + "Backend did not exit gracefully, force killing process tree (pid {})", + pid + ); + force_kill_process_tree(pid, child, "Backend"); + return Ok(()); + } + + #[cfg(unix)] + { + // Force kill the process group on Unix + warn!( + "Backend did not exit gracefully, force killing group (pid {})", + pid + ); + let _ = child.kill(); + + // Reap the process + let _ = child.wait(); + info!("Backend process group forcefully stopped"); + Ok(()) + } +} + +/// Spawn `stop_backend` on a background thread and return immediately. +/// Used by the tray "quit" path so the 5s graceful-wait does not block +/// the Tauri main event loop before `app.exit(0)` fires. +pub fn stop_backend_detached(state: BackendState, shutdown: ShutdownFlag) { + std::thread::spawn(move || { + let _ = stop_backend(&state, &shutdown); + }); +} diff --git a/studio/src-tauri/src/update.rs b/studio/src-tauri/src/update.rs new file mode 100644 index 0000000000..477f18ce28 --- /dev/null +++ b/studio/src-tauri/src/update.rs @@ -0,0 +1,293 @@ +use log::{error, info, warn}; +use process_wrap::std::*; +use std::io::BufRead; +use std::process::{Command, ExitStatus, Stdio}; +use std::sync::{Arc, Mutex}; +use tauri::{AppHandle, Emitter}; + +// ── Types ── + +pub struct UpdateProcess { + pub child: Option>, + pub intentional_stop: bool, +} + +impl Default for UpdateProcess { + fn default() -> Self { + Self { + child: None, + intentional_stop: false, + } + } +} + +pub type UpdateState = Arc>; + +pub fn new_update_state() -> UpdateState { + Arc::new(Mutex::new(UpdateProcess::default())) +} + +// ── Spawn ── + +fn spawn_update( + bin: &std::path::Path, + state: &UpdateState, +) -> Result< + ( + Option, + Option, + ), + String, +> { + let mut update = state.lock().map_err(|e| e.to_string())?; + if update.child.is_some() { + return Err("Update is already running.".to_string()); + } + update.intentional_stop = false; + + let mut cmd = Command::new(bin); + cmd.args(["studio", "update"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + // AppImage sets LD_LIBRARY_PATH to its bundled libs, which breaks Python + #[cfg(target_os = "linux")] + if std::env::var_os("APPIMAGE").is_some() { + cmd.env_remove("LD_LIBRARY_PATH"); + cmd.env_remove("PYTHONHOME"); + cmd.env_remove("PYTHONPATH"); + } + + #[cfg(windows)] + let mut child: Box = { + use std::os::windows::process::CommandExt; + + cmd.creation_flags(crate::process::CREATE_NO_WINDOW); + let child = cmd + .spawn() + .map_err(|e| format!("Failed to spawn update: {}", e))?; + Box::new(child) + }; + + #[cfg(unix)] + let mut child: Box = { + let mut wrap = CommandWrap::from(cmd); + wrap.wrap(ProcessGroup::leader()); + wrap.spawn() + .map_err(|e| format!("Failed to spawn update: {}", e))? + }; + + let stdout = child.stdout().take(); + let stderr = child.stderr().take(); + update.child = Some(child); + Ok((stdout, stderr)) +} + +// ── Stream ── + +fn stream_output( + app: &AppHandle, + progress_event: &'static str, + stdout: Option, + stderr: Option, +) -> Vec> { + let mut threads = Vec::new(); + + if let Some(out) = stdout { + let app_clone = app.clone(); + threads.push(std::thread::spawn(move || { + let reader = std::io::BufReader::new(out); + for line in reader.lines() { + match line { + Ok(text) => { + info!("[update][stdout] {}", text); + let _ = app_clone.emit(progress_event, &text); + } + Err(e) => { + warn!("[update] Error reading stdout: {}", e); + break; + } + } + } + })); + } + + if let Some(err) = stderr { + let app_clone = app.clone(); + threads.push(std::thread::spawn(move || { + let reader = std::io::BufReader::new(err); + for line in reader.lines() { + match line { + Ok(text) => { + warn!("[update][stderr] {}", text); + let _ = app_clone.emit(progress_event, &text); + } + Err(e) => { + warn!("[update] Error reading stderr: {}", e); + break; + } + } + } + })); + } + + threads +} + +// ── Wait ── + +fn wait_for_exit(state: &UpdateState) -> Result<(ExitStatus, bool), String> { + const MAX_WAIT_ITERATIONS: u32 = 72_000; // 2h at 100ms intervals + for _ in 0..MAX_WAIT_ITERATIONS { + let mut update = state.lock().map_err(|e| e.to_string())?; + let intentional = update.intentional_stop; + + match update.child.as_mut() { + Some(child) => match child.try_wait() { + Ok(Some(status)) => { + update.child = None; + return Ok((status, intentional)); + } + Ok(None) => {} + Err(e) => { + update.child = None; + return Err(format!("Error waiting for update: {}", e)); + } + }, + None if intentional => return Err("Update stopped.".to_string()), + None => return Err("Update process disappeared unexpectedly.".to_string()), + } + + drop(update); + std::thread::sleep(std::time::Duration::from_millis(100)); + } + let _ = stop_update(state); + Err("Update timed out after 2 hours".to_string()) +} + +// ── Public API ── + +pub fn run_backend_update(app: AppHandle, state: UpdateState) -> Result<(), String> { + run_backend_update_with_terminal_events(app, state, true) +} + +pub(crate) fn run_backend_update_for_repair( + app: AppHandle, + state: UpdateState, +) -> Result<(), String> { + run_backend_update_with_terminal_events(app, state, false) +} + +fn run_backend_update_with_terminal_events( + app: AppHandle, + state: UpdateState, + terminal_events: bool, +) -> Result<(), String> { + let bin = crate::process::find_unsloth_binary() + .ok_or("Unsloth binary not found. Cannot run update.")?; + + info!("[update] Starting backend update via {:?}", bin); + let progress_event = if terminal_events { + "update-progress" + } else { + "repair-progress" + }; + let _ = app.emit(progress_event, "Starting backend update..."); + + let (stdout, stderr) = spawn_update(&bin, &state)?; + let threads = stream_output(&app, progress_event, stdout, stderr); + + let result = wait_for_exit(&state); + for handle in threads { + let _ = handle.join(); + } + + match result { + Ok((status, _)) if status.success() => { + info!("[update] Backend update complete"); + if terminal_events { + let _ = app.emit("update-complete", ()); + } + Ok(()) + } + Ok((_status, intentional)) if intentional => { + info!("[update] Update stopped intentionally"); + Err("Update stopped.".to_string()) + } + Ok((status, _)) => { + let code = status.code().unwrap_or(-1); + let msg = format!("Update exited with code {}", code); + error!("[update] {}", msg); + if terminal_events { + let _ = app.emit("update-failed", &msg); + } + Err(msg) + } + Err(msg) => { + error!("[update] {}", msg); + if terminal_events { + let _ = app.emit("update-failed", &msg); + } + Err(msg) + } + } +} + +pub fn stop_update(state: &UpdateState) -> Result<(), String> { + let mut child = { + let mut update = match state.lock() { + Ok(guard) => guard, + Err(poisoned) => { + warn!("Update state mutex poisoned, recovering for cleanup"); + poisoned.into_inner() + } + }; + update.intentional_stop = true; + update.child.take() + }; + + let Some(ref mut child) = child else { + return Ok(()); + }; + + let pid = child.id(); + info!("Stopping update process group (pid {})", pid); + + #[cfg(unix)] + { + if pid > i32::MAX as u32 { + warn!("PID {} exceeds i32 range, using direct kill", pid); + let _ = child.kill(); + let _ = child.wait(); + return Ok(()); + } + unsafe { + libc::kill(-(pid as i32), libc::SIGTERM); + } + for _ in 0..50 { + match child.try_wait() { + Ok(Some(status)) => { + info!("Update exited gracefully with status: {:?}", status); + return Ok(()); + } + Ok(None) => std::thread::sleep(std::time::Duration::from_millis(100)), + Err(_) => break, + } + } + warn!("Update did not exit gracefully, force killing"); + } + + #[cfg(windows)] + { + crate::process::force_kill_process_tree(pid, child, "Update"); + return Ok(()); + } + + #[cfg(unix)] + { + let _ = child.kill(); + let _ = child.wait(); + info!("Update process group force stopped"); + Ok(()) + } +} diff --git a/studio/src-tauri/src/windows_job.rs b/studio/src-tauri/src/windows_job.rs new file mode 100644 index 0000000000..411449fb83 --- /dev/null +++ b/studio/src-tauri/src/windows_job.rs @@ -0,0 +1,72 @@ +#[cfg(windows)] +use log::{info, warn}; +#[cfg(windows)] +use std::mem::size_of; +#[cfg(windows)] +use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle}; +#[cfg(windows)] +use std::sync::OnceLock; +#[cfg(windows)] +use windows_sys::Win32::System::JobObjects::{ + AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation, + SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION, + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, +}; +#[cfg(windows)] +use windows_sys::Win32::System::Threading::GetCurrentProcess; + +#[cfg(windows)] +static APP_JOB_INITIALIZED: OnceLock<()> = OnceLock::new(); + +pub fn initialize() { + #[cfg(windows)] + { + if APP_JOB_INITIALIZED.get().is_some() { + return; + } + + match unsafe { create_app_job_object() } { + Ok(()) => { + let _ = APP_JOB_INITIALIZED.set(()); + info!("Windows app job object initialized for crash-safe child cleanup"); + } + Err(err) => { + warn!( + "Failed to initialize Windows app job object; crash cleanup will rely on explicit stop paths: {}", + err + ); + } + } + } +} + +#[cfg(windows)] +unsafe fn create_app_job_object() -> std::io::Result<()> { + let job = CreateJobObjectW(std::ptr::null(), std::ptr::null()); + if job.is_null() { + return Err(std::io::Error::last_os_error()); + } + + let job = OwnedHandle::from_raw_handle(job); + + let mut limits: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed(); + limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + if SetInformationJobObject( + job.as_raw_handle(), + JobObjectExtendedLimitInformation, + &limits as *const _ as *const _, + size_of::() as u32, + ) == 0 + { + return Err(std::io::Error::last_os_error()); + } + + if AssignProcessToJobObject(job.as_raw_handle(), GetCurrentProcess()) == 0 { + return Err(std::io::Error::last_os_error()); + } + + // Keep the job handle alive for the full app lifetime. + std::mem::forget(job); + Ok(()) +} diff --git a/studio/src-tauri/tauri.conf.json b/studio/src-tauri/tauri.conf.json new file mode 100644 index 0000000000..49819f8a25 --- /dev/null +++ b/studio/src-tauri/tauri.conf.json @@ -0,0 +1,66 @@ +{ + "productName": "Unsloth Studio (Desktop)", + "identifier": "ai.unsloth.studio", + "build": { + "beforeDevCommand": "npm --prefix ../frontend run dev -- --port 5173 --strictPort", + "beforeBuildCommand": "npm --prefix ../frontend run build", + "frontendDist": "../frontend/dist", + "devUrl": "http://localhost:5173" + }, + "app": { + "withGlobalTauri": true, + "security": { + "csp": "default-src 'self'; connect-src 'self' http://localhost:* ws://localhost:* ws://127.0.0.1:* http://127.0.0.1:* https://huggingface.co https://*.huggingface.co https://datasets-server.huggingface.co; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; font-src 'self' data:" + }, + "windows": [ + { + "label": "main", + "title": "Unsloth Studio (Desktop)", + "width": 690, + "height": 480, + "visible": false, + "resizable": false + } + ], + "trayIcon": { + "iconPath": "icons/icon.png", + "id": "main", + "tooltip": "Unsloth Studio (Desktop)" + } + }, + "plugins": { + "updater": { + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDJENjJENDYxMTQ1QTYyOEIKUldTTFlsb1VZZFJpTFY1aHgyRkRqZUdjWGg0Sm1BVEZoMDVWME5PdE42bjZiekFRc1Fsb0JNbmIK", + "endpoints": [ + "https://github.com/danielhanchen/unsloth-staging-2/releases/latest/download/latest.json" + ], + "windows": { + "installMode": "passive" + } + } + }, + "bundle": { + "active": true, + "createUpdaterArtifacts": true, + "targets": ["app", "appimage", "deb", "dmg", "nsis"], + "resources": { + "../../install.sh": "install.sh", + "../../install.ps1": "install.ps1", + "../install_python_stack.py": "install_python_stack.py" + }, + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/icon.ico", + "icons/icon.icns" + ], + "linux": { + "deb": { + "postRemoveScript": "./linux/postremove.sh" + }, + "rpm": { + "postRemoveScript": "./linux/postremove.sh" + } + } + } +} diff --git a/studio/src-tauri/tauri.macos.conf.json b/studio/src-tauri/tauri.macos.conf.json new file mode 100644 index 0000000000..202c9fb505 --- /dev/null +++ b/studio/src-tauri/tauri.macos.conf.json @@ -0,0 +1,11 @@ +{ + "bundle": { + "macOS": { + "entitlements": "./Entitlements.plist", + "dmg": { + "appPosition": { "x": 180, "y": 220 }, + "applicationFolderPosition": { "x": 480, "y": 220 } + } + } + } +} diff --git a/studio/src-tauri/tauri.windows.conf.json b/studio/src-tauri/tauri.windows.conf.json new file mode 100644 index 0000000000..a68cf6351f --- /dev/null +++ b/studio/src-tauri/tauri.windows.conf.json @@ -0,0 +1,16 @@ +{ + "bundle": { + "windows": { + "signCommand": { + "cmd": "trusted-signing-cli", + "args": ["-e", "https://eus.codesigning.azure.net", "-d", "Unsloth Studio (Desktop)", "%1"] + }, + "nsis": { + "installerHooks": "./windows/hooks.nsh", + "installerIcon": "./icons/icon.ico", + "headerImage": "./windows/branding/nsis-header.bmp", + "sidebarImage": "./windows/branding/nsis-sidebar.bmp" + } + } + } +} diff --git a/studio/src-tauri/windows/branding/nsis-header.bmp b/studio/src-tauri/windows/branding/nsis-header.bmp new file mode 100644 index 0000000000000000000000000000000000000000..7eacdce7db5026855113aa5f03c47663d1e17561 GIT binary patch literal 25818 zcmeI2M`%?|6vyAoYkB>pM+JLBK}E5|0vb>h4T7NJ!i|2eT)0utg{~Ag7ElNZE-XP% z(S>}9pkT*}9g&D2*s&q@{{7~jWHPylFYkru{S0%txlTEA=FIt@|CyPX>YDn0e9kk9 z??8TP`KjW^@2lkN^VK!5v^=ytv^@0Q1D1!Dhu%k_-487fEe|aZz4w6S zq2;0XQE2x=%R|dU%R}!y&|V&vrBfxTL}@Bno=K6-_?Bf-Ql?5-Rhx&nOGb?#ji!lcqkA+Bk+m_+K~?jgGeLHC}w2crhdxBsRZpxXMrDW1R8ds zIUSFgl2mC#TdT2v+QM)cC4PUypZ-y3cpLv9ke;T-Mlm{FVj|uo+;X^XQ64CW7e-n& z$uz!YIe!dekx1jj4ga!O9xAa&SR5%;m*;RQGWZ79CY>@>h6ya9i$$OzlZdGc|p@`v9Gjd(F{i>QLhB#Hs$7-C!iusD4FfexvNmHzjaDZ|| z9(;p8Xuy9|AvR^fu(BwfbX-mg%z=&c5)R^NzM898qDJ|A(MXsmAkU*p#B#)t)rmYS znmTprjvYHroH(&(&z{+{XX9yKw`AMUUTwKDWMopH z6HCadWFcd_ai6L8@FyBm@vLn z=#UU;*REakx-cYXo;-Q-$B!Q~X3Ss|Ec*26)77h2d-m*k@#01JHfho%+DDHb4ea;t z--n9VuV06&)z#HUjvN8BMT-{6@=2ud`N@+flBHR)Yq43X`N*P1+}1t};T!fk99H)d zP9#)u3`AmoV}}F^vDCy6NpUR)5!X5-c{LTj^y&1DA3vTnd+pk_UcGt=hG0ee;K74mzI^%k z@nh&G7*3u%nVl94=ggVoOy~o;r%#_EhOo3~YuhaRP7FC)XjsTpl5b_df`nK~*mFj~ zMjXtm?i`M%O$^b%5QO=nL@x*pg00>9EN4Ax^M(l)+Gz*M!Hm3HS&-X2@d9{ z0g=FG!GZ-NMvPzx`=OajT^Pbc7lsojPGr>L#f#B)?%cUPefmf)9XD>ABu~C_T4KM2 z&){ELTFMYC(JO7U@H;WY7U39iFF4R5E_V_aH6L9#kE+Oko-3D91!rLhX_G@0=s{_r zF*v{_1W+qSN?IuMqEl~iOoRbU!7BjLLWr5}(ZNEOc>VI_%gdH6;|l_-SFhf%VFO3X zH*em2`}U0-yKURHv17-QOX(FJN-l+I@R0qG_5%kF?A*EY)vH&ZKYu0=IMdv^bqk!y zu8f81)J_pb=#-pJVE;^Ix4Hj&)$H+vN5afqp} zuV-5W5FU^u@6DPu>)^qIFJ8Q0^JEmMl7xBc)Ttgldca|1c-ZAQqq6UDBm%>;XU{%* z^hl1iGiT1^uM zN#iaLZa5pCA!F`8M1kU-(~AJZmDin1w+lmA0osU8Bpq$sEzzbx0AcQ#43|vEm#O*f z40P2PGLcxWh6kMA@ihJhNR-Q079xRw%Lfb?09&})VPiNPW~buhp&!nZTo?QH?Moq* zN~x%c$A=6V!YG6pC6bIPDJd~^B7>x~fGSxU@Vj;ECM~3MoMu|pW;stDqK}0V`Yy$^ z(5@RDnT9H5d};1HT1>Rj%WDpA6+b^I;)TpZAD40HG2|3R`7Vc6207DMAw+|oV2wFTcpD&F?x7Hu0F zAhbk}{Qrk;?_`DlB@a89kAL@aEQa2tn~uWPVrVgRUlcnkwyd7qJz#lgd1!g)y$380 zEf2kq!rb?b&MIknXnAOP=)DIl4=oS9kHXHn%jLc<%R|dU%R}!yV0mbH=zSFCzHf9^ TNy|gaL(4<&Jz#m*8F=^~mCb?X literal 0 HcmV?d00001 diff --git a/studio/src-tauri/windows/branding/nsis-sidebar.bmp b/studio/src-tauri/windows/branding/nsis-sidebar.bmp new file mode 100644 index 0000000000000000000000000000000000000000..c3bd480fc8993fa88d280c35ad105a7ddf1bc7ff GIT binary patch literal 154542 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 start && /exit 0/ { print NR; exit }' "$INSTALL_SH") +tauri_done_line=$(awk -v start="$tauri_marker_line" 'NR > start && /tauri_log "DONE" ""/ { print NR; exit }' "$INSTALL_SH") +first_setup_check_line=$(grep -n 'if \[ "$_SETUP_EXIT" -ne 0 \]; then' "$INSTALL_SH" | head -n1 | cut -d: -f1) +setup_exit_line=$(awk -v start="$first_setup_check_line" 'NR > start && /exit "\$_SETUP_EXIT"/ { print NR; exit }' "$INSTALL_SH") +shortcut_line=$(grep -n 'create_studio_shortcuts "$VENV_ABS_BIN/unsloth" "$OS"' "$INSTALL_SH" | head -n1 | cut -d: -f1) +shortcut_guard_line=$( + awk -v stop="$first_setup_check_line" ' + NR >= stop { exit } + /\[ "\$TAURI_MODE" != true \]/ { + print NR + exit + } + ' "$INSTALL_SH" +) +shortcut_guard_end_line=$( + awk -v start="$shortcut_guard_line" -v shortcut="$shortcut_line" ' + NR <= start { next } + NR <= shortcut { next } + /^[[:space:]]*fi[[:space:]]*$/ { + print NR + exit + } + ' "$INSTALL_SH" +) +early_tauri_exit_line=$( + awk -v setup="$first_setup_check_line" ' + NR >= setup { exit } + /\[ "\$TAURI_MODE" = true \]/ { + in_tauri = 1 + depth = 1 + next + } + in_tauri && /^[[:space:]]*if[[:space:]].*;[[:space:]]*then[[:space:]]*$/ { + depth++ + } + in_tauri && /exit[[:space:]]+0/ { + print NR + exit + } + in_tauri && /^[[:space:]]*fi[[:space:]]*$/ { + depth-- + if (depth == 0) { + in_tauri = 0 + } + } + ' "$INSTALL_SH" +) + +if [ -z "$tauri_marker_line" ] || [ -z "$tauri_exit_line" ] || [ -z "$tauri_done_line" ] || [ -z "$first_setup_check_line" ] || [ -z "$setup_exit_line" ] || [ -z "$shortcut_line" ] || [ -z "$shortcut_guard_line" ] || [ -z "$shortcut_guard_end_line" ]; then + echo " FAIL: required install.sh markers not found" + exit 1 +fi + +if [ -n "$early_tauri_exit_line" ]; then + echo " FAIL: Tauri success exit before setup failure check at line $early_tauri_exit_line" + exit 1 +fi + +if [ "$shortcut_guard_line" -ge "$shortcut_line" ] || [ "$shortcut_line" -ge "$shortcut_guard_end_line" ]; then + echo " FAIL: shortcuts are not guarded by non-Tauri check" + exit 1 +fi + +if [ "$shortcut_line" -ge "$first_setup_check_line" ]; then + echo " FAIL: shortcut line $shortcut_line is after setup failure check line $first_setup_check_line" + exit 1 +fi + +if [ "$first_setup_check_line" -ge "$setup_exit_line" ]; then + echo " FAIL: setup failure check line $first_setup_check_line is after setup exit line $setup_exit_line" + exit 1 +fi + +if [ "$setup_exit_line" -ge "$tauri_done_line" ]; then + echo " FAIL: setup failure exit line $setup_exit_line is after Tauri DONE line $tauri_done_line" + exit 1 +fi + +if [ "$setup_exit_line" -ge "$tauri_exit_line" ]; then + echo " FAIL: setup failure check line $first_setup_check_line is after Tauri exit line $tauri_exit_line" + exit 1 +fi + +echo " PASS: non-Tauri shortcuts run before setup failure exit" +echo " PASS: setup failure exits before Tauri success" +echo " PASS: Tauri skips shortcuts" diff --git a/unsloth_cli/commands/studio.py b/unsloth_cli/commands/studio.py index a3c0840be1..b74f42674d 100644 --- a/unsloth_cli/commands/studio.py +++ b/unsloth_cli/commands/studio.py @@ -1,11 +1,19 @@ # 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 importlib.util +import hashlib +import json import os import platform +import secrets +import sqlite3 import subprocess import sys +import tempfile import time +import types +from datetime import datetime, timezone from pathlib import Path from typing import Optional import typer @@ -13,12 +21,52 @@ import typer studio_app = typer.Typer(help = "Unsloth Studio commands.") STUDIO_HOME = Path.home() / ".unsloth" / "studio" +BOOTSTRAP_PASSWORD_FILE = ".bootstrap_password" +DESKTOP_SECRET_FILE = ".desktop_secret" +DEFAULT_ADMIN_USERNAME = "unsloth" +DESKTOP_SECRET_PREFIX = "desktop-" +API_KEY_PBKDF2_SALT_KEY = "api_key_pbkdf2_salt" +DESKTOP_SECRET_HASH_KEY = "desktop_secret_hash" +DESKTOP_SECRET_CREATED_AT_KEY = "desktop_secret_created_at" +PBKDF2_ITERATIONS = 100_000 # __file__ is unsloth_cli/commands/studio.py -- two parents up is the package root # (either site-packages or the repo root for editable installs). _PACKAGE_ROOT = Path(__file__).resolve().parent.parent.parent +def _should_hide_windows_subprocesses() -> bool: + """Hide child console windows only for non-interactive Windows launches.""" + if platform.system() != "Windows": + return False + try: + return not sys.stdout.isatty() + except (AttributeError, OSError, ValueError): + return True + + +def _windows_hidden_subprocess_kwargs() -> dict[str, object]: + """Return Windows-only Popen kwargs that suppress transient console windows.""" + if not _should_hide_windows_subprocesses(): + return {} + + kwargs: dict[str, object] = {} + create_no_window = getattr(subprocess, "CREATE_NO_WINDOW", 0) + if create_no_window: + kwargs["creationflags"] = create_no_window + + startupinfo_factory = getattr(subprocess, "STARTUPINFO", None) + startf_use_showwindow = getattr(subprocess, "STARTF_USESHOWWINDOW", 0) + sw_hide = getattr(subprocess, "SW_HIDE", 0) + if startupinfo_factory is not None and startf_use_showwindow: + startupinfo = startupinfo_factory() + startupinfo.dwFlags |= startf_use_showwindow + startupinfo.wShowWindow = sw_hide + kwargs["startupinfo"] = startupinfo + + return kwargs + + def _studio_venv_python() -> Optional[Path]: """Return the studio venv Python binary, or None if not set up.""" if platform.system() == "Windows": @@ -97,12 +145,228 @@ def _create_api_key_inprocess(name: str) -> str: ``POST /api/auth/api-keys`` on fresh installs. Safe because the CLI already has filesystem access to ``~/.unsloth/studio``. """ - from auth.storage import create_api_key, DEFAULT_ADMIN_USERNAME + storage = _load_backend_auth_storage() - raw_key, _row = create_api_key(username = DEFAULT_ADMIN_USERNAME, name = name) + raw_key, _row = storage.create_api_key( + username = storage.DEFAULT_ADMIN_USERNAME, + name = name, + ) return raw_key +def _load_backend_auth_storage(): + run_py = _find_run_py() + backend_dir = ( + run_py.parent if run_py is not None else _PACKAGE_ROOT / "studio" / "backend" + ) + if backend_dir.is_dir() and str(backend_dir) not in sys.path: + sys.path.insert(0, str(backend_dir)) + + auth_dir = backend_dir / "auth" + storage_py = auth_dir / "storage.py" + loaded = sys.modules.get("auth.storage") + loaded_path = Path(getattr(loaded, "__file__", "")).resolve() + if loaded is not None and loaded_path == storage_py: + return loaded + + package = sys.modules.get("auth") + package_paths = [Path(path).resolve() for path in getattr(package, "__path__", [])] + if package is None or auth_dir.resolve() not in package_paths: + package = types.ModuleType("auth") + package.__path__ = [str(auth_dir)] + package.__package__ = "auth" + package.__file__ = str(auth_dir / "__init__.py") + sys.modules["auth"] = package + + spec = importlib.util.spec_from_file_location("auth.storage", storage_py) + if spec is None or spec.loader is None: + raise ImportError(f"Could not load backend auth storage from {storage_py}") + storage = importlib.util.module_from_spec(spec) + sys.modules["auth.storage"] = storage + spec.loader.exec_module(storage) + + return storage + + +def _write_auth_secret(path: Path, secret: str) -> None: + path.parent.mkdir(parents = True, exist_ok = True) + fd, tmp_name = tempfile.mkstemp(prefix = f".{path.name}.", dir = path.parent) + tmp_path = Path(tmp_name) + try: + try: + os.chmod(tmp_path, 0o600) + except OSError: + pass + with os.fdopen(fd, "w") as f: + fd = -1 + f.write(secret) + os.replace(tmp_path, path) + except Exception: + if fd >= 0: + os.close(fd) + tmp_path.unlink(missing_ok = True) + raise + try: + os.chmod(path, 0o600) + except OSError: + pass + + +def _connect_auth_db() -> sqlite3.Connection: + auth_dir = STUDIO_HOME / "auth" + auth_dir.mkdir(parents = True, exist_ok = True) + conn = sqlite3.connect(auth_dir / "auth.db") + conn.execute( + """ + CREATE TABLE IF NOT EXISTS auth_user ( + id INTEGER PRIMARY KEY, + username TEXT UNIQUE NOT NULL, + password_salt TEXT NOT NULL, + password_hash TEXT NOT NULL, + jwt_secret TEXT NOT NULL, + must_change_password INTEGER NOT NULL DEFAULT 0 + ); + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS refresh_tokens ( + id INTEGER PRIMARY KEY, + token_hash TEXT NOT NULL, + username TEXT NOT NULL, + expires_at TEXT NOT NULL, + is_desktop INTEGER NOT NULL DEFAULT 0 + ); + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS api_keys ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL, + key_prefix TEXT NOT NULL, + key_hash TEXT NOT NULL UNIQUE, + name TEXT NOT NULL DEFAULT '', + created_at TEXT NOT NULL, + last_used_at TEXT, + expires_at TEXT, + is_active INTEGER NOT NULL DEFAULT 1 + ); + """ + ) + conn.execute( + """ + CREATE TABLE IF NOT EXISTS app_secrets ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + """ + ) + auth_columns = {row[1] for row in conn.execute("PRAGMA table_info(auth_user)")} + if "must_change_password" not in auth_columns: + conn.execute( + "ALTER TABLE auth_user ADD COLUMN must_change_password INTEGER NOT NULL DEFAULT 0" + ) + refresh_columns = { + row[1] for row in conn.execute("PRAGMA table_info(refresh_tokens)") + } + if "is_desktop" not in refresh_columns: + conn.execute( + "ALTER TABLE refresh_tokens ADD COLUMN is_desktop INTEGER NOT NULL DEFAULT 0" + ) + conn.commit() + return conn + + +def _pbkdf2_hex(value: str, salt: bytes) -> str: + return hashlib.pbkdf2_hmac( + "sha256", + value.encode("utf-8"), + salt, + PBKDF2_ITERATIONS, + ).hex() + + +def _hash_password(password: str) -> tuple[str, str]: + salt = secrets.token_hex(16) + pwd_hash = _pbkdf2_hex(password, salt.encode("utf-8")) + return salt, pwd_hash + + +def _get_or_create_api_key_pbkdf2_salt(conn: sqlite3.Connection) -> bytes: + row = conn.execute( + "SELECT value FROM app_secrets WHERE key = ?", + (API_KEY_PBKDF2_SALT_KEY,), + ).fetchone() + if row is None: + salt_hex = secrets.token_hex(32) + conn.execute( + "INSERT OR IGNORE INTO app_secrets (key, value) VALUES (?, ?)", + (API_KEY_PBKDF2_SALT_KEY, salt_hex), + ) + row = conn.execute( + "SELECT value FROM app_secrets WHERE key = ?", + (API_KEY_PBKDF2_SALT_KEY,), + ).fetchone() + return bytes.fromhex(row[0]) + + +def _ensure_cli_default_admin(conn: sqlite3.Connection) -> None: + row = conn.execute( + "SELECT 1 FROM auth_user WHERE username = ?", + (DEFAULT_ADMIN_USERNAME,), + ).fetchone() + if row is not None: + return + + bootstrap_password = secrets.token_urlsafe(32) + password_salt, password_hash = _hash_password(bootstrap_password) + conn.execute( + """ + INSERT INTO auth_user ( + username, + password_salt, + password_hash, + jwt_secret, + must_change_password + ) + VALUES (?, ?, ?, ?, ?) + """, + ( + DEFAULT_ADMIN_USERNAME, + password_salt, + password_hash, + secrets.token_urlsafe(64), + 1, + ), + ) + _write_auth_secret( + STUDIO_HOME / "auth" / BOOTSTRAP_PASSWORD_FILE, + bootstrap_password, + ) + + +def _create_desktop_secret_in_cli() -> str: + raw_secret = DESKTOP_SECRET_PREFIX + secrets.token_urlsafe(48) + now = datetime.now(timezone.utc).isoformat() + conn = _connect_auth_db() + try: + _ensure_cli_default_admin(conn) + secret_hash = _pbkdf2_hex(raw_secret, _get_or_create_api_key_pbkdf2_salt(conn)) + conn.execute( + "INSERT OR REPLACE INTO app_secrets (key, value) VALUES (?, ?)", + (DESKTOP_SECRET_HASH_KEY, secret_hash), + ) + conn.execute( + "INSERT OR REPLACE INTO app_secrets (key, value) VALUES (?, ?)", + (DESKTOP_SECRET_CREATED_AT_KEY, now), + ) + conn.commit() + return raw_secret + finally: + conn.close() + + def _load_model_via_http( port: int, api_key: str, @@ -153,6 +417,11 @@ def studio_default( host: str = typer.Option("0.0.0.0", "--host", "-H"), frontend: Optional[Path] = typer.Option(None, "--frontend", "-f"), silent: bool = typer.Option(False, "--silent", "-q"), + api_only: bool = typer.Option( + False, + "--api-only", + help = "Run API server only, no frontend serving (for Tauri desktop app)", + ), ): """Launch the Unsloth Studio server.""" if ctx.invoked_subcommand is not None: @@ -180,13 +449,15 @@ def studio_default( args.extend(["--frontend", str(frontend)]) if silent: args.append("--silent") + if api_only: + args.append("--api-only") # 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) + proc = _sp.Popen(args, **_windows_hidden_subprocess_kwargs()) try: rc = proc.wait() except KeyboardInterrupt: @@ -217,7 +488,7 @@ def studio_default( 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) + run_kwargs = dict(host = host, port = port, silent = silent, api_only = api_only) if frontend is not None: run_kwargs["frontend_path"] = frontend run_server(**run_kwargs) @@ -489,9 +760,16 @@ def _run_setup_script(*, verbose: bool = False) -> None: env = {**os.environ, "UNSLOTH_VERBOSE": "1"} if verbose else None if platform.system() == "Windows": + powershell_args = ["powershell.exe"] + if _should_hide_windows_subprocesses(): + powershell_args.extend( + ["-NoLogo", "-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden"] + ) + powershell_args.extend(["-ExecutionPolicy", "Bypass", "-File", str(script)]) result = subprocess.run( - ["powershell", "-ExecutionPolicy", "Bypass", "-File", str(script)], + powershell_args, env = env, + **_windows_hidden_subprocess_kwargs(), ) else: result = subprocess.run(["bash", str(script)], env = env) @@ -547,6 +825,44 @@ def update( # ── unsloth studio reset-password ──────────────────────────────────── +@studio_app.command("desktop-capabilities", hidden = True) +def desktop_capabilities( + json_output: bool = typer.Option( + False, + "--json", + help = "Emit machine-readable JSON.", + ), +): + payload = { + "desktop_protocol_version": 1, + "supports_provision_desktop_auth": True, + "supports_api_only": True, + "version": "unknown", + } + try: + from importlib.metadata import version as package_version + + payload["version"] = package_version("unsloth") + except Exception: + pass + + if json_output: + typer.echo(json.dumps(payload, sort_keys = True)) + return + + for key, value in payload.items(): + typer.echo(f"{key}: {value}") + + +@studio_app.command("provision-desktop-auth", hidden = True) +def provision_desktop_auth(): + """Create/repair desktop auth state for the local machine.""" + auth_dir = STUDIO_HOME / "auth" + secret = _create_desktop_secret_in_cli() + _write_auth_secret(auth_dir / DESKTOP_SECRET_FILE, secret) + typer.echo("Desktop auth ready.") + + @studio_app.command("reset-password") def reset_password(): """Reset the Studio admin password. @@ -557,13 +873,18 @@ def reset_password(): """ auth_dir = STUDIO_HOME / "auth" db_file = auth_dir / "auth.db" - pw_file = auth_dir / ".bootstrap_password" + stale_files = [ + auth_dir / BOOTSTRAP_PASSWORD_FILE, + auth_dir / DESKTOP_SECRET_FILE, + ] + had_db = db_file.exists() - if not db_file.exists(): + db_file.unlink(missing_ok = True) + for path in stale_files: + path.unlink(missing_ok = True) + + if not had_db: typer.echo("No auth database found -- nothing to reset.") raise typer.Exit(0) - db_file.unlink(missing_ok = True) - pw_file.unlink(missing_ok = True) - typer.echo("Auth database deleted. Restart Unsloth Studio to get a new password.") From ad6bd780a91f4db03685c0666373facc8f44fbfe Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 23 Apr 2026 06:42:12 -0700 Subject: [PATCH 7/8] 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 2e7e17bbb9..8b8dec3d3e 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.4.7" +__version__ = "2026.4.8" __all__ = [ "SUPPORTS_BFLOAT16", From 563fcf89526cde9bb1254c385839e15f01cdf613 Mon Sep 17 00:00:00 2001 From: Daniel Han Date: Thu, 23 Apr 2026 06:57:36 -0700 Subject: [PATCH 8/8] Studio: detect reasoning_effort and preserve_thinking in chat templates (#5149) * Studio: detect reasoning_effort and preserve_thinking in chat templates Previously Studio's chat template sniffer only recognized Qwen's enable_thinking and DeepSeek's thinking markers. For gpt-oss (Harmony templates) and newer Qwen3.6 templates, the Think toggle was hidden or could only be flipped on/off. This change adds two new detections and corresponding UI controls: 1. reasoning_effort style (gpt-oss). When the chat template contains reasoning_effort, the Think button becomes a Low / Medium / High dropdown and the backend forwards {"reasoning_effort": } in chat_template_kwargs. Load-time --chat-template-kwargs flag is also switched to the new style. 2. preserve_thinking kwarg (Qwen3.6). Independent of the reasoning toggle. When the template mentions preserve_thinking, a new Preserve Thinking on/off pill is shown next to Think. Off by default, persisted via localStorage. When on, the backend adds {"preserve_thinking": true} to chat_template_kwargs so past-turn blocks are kept in the prompt instead of being stripped. Backend helper _request_reasoning_kwargs now merges all applicable kwargs into a single chat_template_kwargs dict based on the model's detected style and template capabilities. Inputs are validated with Literal types in the Pydantic request model. Tested end to end against cached GGUFs for unsloth/gpt-oss-20b-GGUF and unsloth/Qwen3.6-35B-A3B-GGUF. Confirmed the llama-server startup --chat-template-kwargs flag and per-request JSON body carry the expected keys for all combinations. * Studio: review pass and CI format fixes for reasoning-styles PR Addresses review feedback and pre-commit CI: - Preserve Thinking pill in shared-composer now gates on modelLoaded only, matching the thread.tsx toggle. Previously the inline version disabled whenever supports_reasoning was false. - The non-GGUF already_loaded LoadResponse now emits reasoning_style (and supports_preserve_thinking=False) so a reconnecting frontend sees the correct style for an already-running gpt-oss safetensors model. - use-chat-model-runtime reconnect path now always clears reasoningEnabled for models without reasoning support instead of inheriting the previous model's state. - _reasoning_default is now reset alongside the other reasoning flags in both backend reset blocks. - supports_reasoning description updated to mention reasoning_effort alongside enable_thinking. - Ran scripts/run_ruff_format.py on the touched Python files to satisfy pre-commit.ci. * Studio: detect reasoning flags on safetensors load + share Qwen param helper Addresses bot review feedback: - Extract the chat-template substring sniffer out of _read_gguf_metadata into a module-level detect_reasoning_flags(template, model_id) helper. Also runs on the safetensors / transformers load paths: - POST /api/inference/load non-GGUF LoadResponse - already_loaded non-GGUF early return - GET /api/inference/status non-GGUF branch The gpt-oss fallback via backend._is_gpt_oss_model() is preserved so safetensors gpt-oss still surfaces reasoning controls even when no chat_template is stored on the model record. - Deduplicate the Qwen3 / Qwen3.5 / Qwen3.6 Think-toggle parameter adjustment into a single features/chat/utils/qwen-params.ts. Both the assistant-ui Think toggle (thread.tsx) and the shared composer (shared-composer.tsx) now import the same helper. The superset that applies presence_penalty=1.5 for Qwen3.5 and Qwen3.6 is now used by both sites (thread.tsx previously did not apply it). * Studio: fill missing reasoning flags on safetensors status + add always_on reset Round 4 review fixes: - routes/inference.py safetensors status response now populates reasoning_always_on and supports_tools from detect_reasoning_flags. Previously Pydantic defaulted both to False, so safetensors models with always-on templates or tool-calling templates were silently losing those flags on /api/inference/status reconnect. - routes/inference.py already_loaded safetensors branch now falls back to backend._is_gpt_oss_model() when the chat template is missing, matching the status-endpoint behaviour. - Safetensors status endpoint log_source set to "Safetensors status" so the emitted template-detection log lines are attributable. - chat-runtime-store clearCheckpoint now also resets reasoningAlwaysOn so switching from an always-on reasoning model to a non-always-on one does not leave the Think button permanently locked on. * Studio: skip reasoning kwargs when always-on; narrow non-GGUF advertisement Round 5 addresses reviewer feedback: - _request_reasoning_kwargs and the load-time --chat-template-kwargs emission now skip when _reasoning_always_on is true. Templates with hardcoded tags do not consume enable_thinking / reasoning_effort so sending them was noise. - Non-GGUF (Unsloth / transformers) LoadResponse and InferenceStatusResponse paths no longer advertise template-derived supports_reasoning / reasoning_style / supports_preserve_thinking / supports_tools. The transformers generation path does not yet forward chat_template_kwargs to tokenizer.apply_chat_template, so exposing the UI controls on those models was misleading. Only the gpt-oss Harmony case is kept (reasoning_style = reasoning_effort) because it is handled via the HarmonyTextStreamer at the tokenizer level. A follow-up PR can thread chat_template_kwargs through the transformers path and re-enable the broader detection. - GGUF / llama-server paths keep the full detect_reasoning_flags output. --- studio/backend/core/inference/llama_cpp.py | 218 +++++++++++++----- studio/backend/models/inference.py | 26 ++- studio/backend/routes/inference.py | 69 +++++- .../src/components/assistant-ui/thread.tsx | 99 +++++++- .../src/features/chat/api/chat-adapter.ts | 28 ++- .../chat/hooks/use-chat-model-runtime.ts | 13 ++ .../src/features/chat/shared-composer.tsx | 140 ++++++++--- .../chat/stores/chat-runtime-store.ts | 47 ++++ .../frontend/src/features/chat/types/api.ts | 6 + .../src/features/chat/utils/qwen-params.ts | 29 +++ 10 files changed, 566 insertions(+), 109 deletions(-) create mode 100644 studio/frontend/src/features/chat/utils/qwen-params.ts diff --git a/studio/backend/core/inference/llama_cpp.py b/studio/backend/core/inference/llama_cpp.py index 6ad05ac52d..c320f03b2c 100644 --- a/studio/backend/core/inference/llama_cpp.py +++ b/studio/backend/core/inference/llama_cpp.py @@ -87,6 +87,84 @@ _TC_PARAM_START_RE = re.compile(r"\s*") _TC_PARAM_CLOSE_RE = re.compile(r"\s*\s*$") +_TOOL_TEMPLATE_MARKERS = ( + "{%- if tools %}", + "{%- if tools -%}", + "{% if tools %}", + "{% if tools -%}", + '"role" == "tool"', + "'role' == 'tool'", + 'message.role == "tool"', + "message.role == 'tool'", +) + + +def detect_reasoning_flags( + chat_template: Optional[str], + model_identifier: Optional[str] = None, + *, + log_source: Optional[str] = None, +) -> dict: + """Classify a chat template's reasoning and tool-calling capabilities. + + Returns a dict with the same five keys populated by the GGUF sniffer: + ``supports_reasoning``, ``reasoning_style`` + (``"enable_thinking"`` | ``"reasoning_effort"``), + ``reasoning_always_on``, ``supports_preserve_thinking``, and + ``supports_tools``. Used by both the llama-server backend at load + time and the safetensors/transformers paths in ``routes/inference`` + so the two agree on what the frontend will see. + """ + flags = { + "supports_reasoning": False, + "reasoning_style": "enable_thinking", + "reasoning_always_on": False, + "supports_preserve_thinking": False, + "supports_tools": False, + } + if not chat_template: + return flags + tpl = chat_template + prefix = f"{log_source}: " if log_source else "" + + if "enable_thinking" in tpl: + flags["supports_reasoning"] = True + flags["reasoning_style"] = "enable_thinking" + logger.info(f"{prefix}model supports reasoning (enable_thinking)") + elif "reasoning_effort" in tpl: + # gpt-oss / Harmony templates use reasoning_effort + # ("low" | "medium" | "high") instead of a boolean. + flags["supports_reasoning"] = True + flags["reasoning_style"] = "reasoning_effort" + logger.info(f"{prefix}model supports reasoning (reasoning_effort)") + elif "thinking" in tpl: + # DeepSeek uses 'thinking' instead of 'enable_thinking' + normalized_id = (model_identifier or "").lower() + if "deepseek" in normalized_id: + flags["supports_reasoning"] = True + logger.info(f"{prefix}model supports reasoning (DeepSeek thinking)") + + # Hardcoded tags or reasoning_content in the template mean + # thinking is always on (no toggle to disable it). + if not flags["supports_reasoning"]: + if ("" in tpl and "" in tpl) or "reasoning_content" in tpl: + flags["supports_reasoning"] = True + flags["reasoning_always_on"] = True + logger.info(f"{prefix}model always reasons ( tags in template)") + + # preserve_thinking is an independent kwarg on some Qwen templates + # that keeps historical blocks in prior assistant turns. + if "preserve_thinking" in tpl: + flags["supports_preserve_thinking"] = True + logger.info(f"{prefix}model supports preserve_thinking") + + if any(marker in tpl for marker in _TOOL_TEMPLATE_MARKERS): + flags["supports_tools"] = True + logger.info(f"{prefix}model supports tool calling") + + return flags + + class LlamaCppBackend: """ Manages a llama-server subprocess for GGUF model inference. @@ -112,6 +190,8 @@ class LlamaCppBackend: self._chat_template: Optional[str] = None self._supports_reasoning: bool = False self._reasoning_always_on: bool = False + self._reasoning_style: str = "enable_thinking" + self._supports_preserve_thinking: bool = False self._supports_tools: bool = False self._cache_type_kv: Optional[str] = None self._reasoning_default: bool = True @@ -293,10 +373,51 @@ class LlamaCppBackend: def reasoning_always_on(self) -> bool: return self._reasoning_always_on + @property + def reasoning_style(self) -> str: + return self._reasoning_style + + @property + def supports_preserve_thinking(self) -> bool: + return self._supports_preserve_thinking + @property def reasoning_default(self) -> bool: return self._reasoning_default + def _reasoning_kwargs(self, enable_thinking: bool) -> dict: + if self._reasoning_style == "reasoning_effort": + return {"reasoning_effort": "high" if enable_thinking else "low"} + return {"enable_thinking": enable_thinking} + + def _request_reasoning_kwargs( + self, + enable_thinking: Optional[bool], + reasoning_effort: Optional[str] = None, + preserve_thinking: Optional[bool] = None, + ) -> Optional[dict]: + """Build chat_template_kwargs from per-request reasoning fields. + + Produces a merged dict covering the active model's reasoning style + (``enable_thinking`` or ``reasoning_effort``) plus the independent + ``preserve_thinking`` kwarg when the template supports it. + """ + kwargs: dict = {} + # Always-on reasoning models hardcode tags in their template + # and do not consume enable_thinking / reasoning_effort -- skip. + if self._supports_reasoning and not self._reasoning_always_on: + if self._reasoning_style == "reasoning_effort": + if reasoning_effort in ("low", "medium", "high"): + kwargs["reasoning_effort"] = reasoning_effort + elif enable_thinking is not None: + kwargs["reasoning_effort"] = "high" if enable_thinking else "low" + else: + if enable_thinking is not None: + kwargs["enable_thinking"] = enable_thinking + if self._supports_preserve_thinking and preserve_thinking is not None: + kwargs["preserve_thinking"] = preserve_thinking + return kwargs or None + @property def supports_tools(self) -> bool: return self._supports_tools @@ -818,6 +939,9 @@ class LlamaCppBackend: self._chat_template = None self._supports_reasoning = False self._reasoning_always_on = False + self._reasoning_style = "enable_thinking" + self._reasoning_default = True + self._supports_preserve_thinking = False self._supports_tools = False self._n_layers = None self._n_kv_heads = None @@ -898,48 +1022,16 @@ class LlamaCppBackend: f"GGUF metadata: chat_template={len(self._chat_template)} chars" ) # Detect thinking/reasoning support from chat template - tpl = self._chat_template - if "enable_thinking" in tpl: - self._supports_reasoning = True - logger.info( - "GGUF metadata: model supports reasoning (enable_thinking)" - ) - elif "thinking" in tpl: - # DeepSeek uses 'thinking' instead of 'enable_thinking' - normalized_id = (self._model_identifier or "").lower() - if "deepseek" in normalized_id: - self._supports_reasoning = True - logger.info( - "GGUF metadata: model supports reasoning (DeepSeek thinking)" - ) - # Models with hardcoded tags or reasoning_content - # in their chat template always produce thinking output - # (no toggle to disable it). - if not self._supports_reasoning: - if ( - "" in tpl - and "" in tpl - or "reasoning_content" in tpl - ): - self._supports_reasoning = True - self._reasoning_always_on = True - logger.info( - "GGUF metadata: model always reasons ( tags in template)" - ) - # Detect tool calling support from chat template - tool_markers = [ - "{%- if tools %}", - "{%- if tools -%}", - "{% if tools %}", - "{% if tools -%}", - '"role" == "tool"', - "'role' == 'tool'", - 'message.role == "tool"', - "message.role == 'tool'", - ] - if any(marker in tpl for marker in tool_markers): - self._supports_tools = True - logger.info("GGUF metadata: model supports tool calling") + flags = detect_reasoning_flags( + self._chat_template, + self._model_identifier, + log_source = "GGUF metadata", + ) + self._supports_reasoning = flags["supports_reasoning"] + self._reasoning_style = flags["reasoning_style"] + self._reasoning_always_on = flags["reasoning_always_on"] + self._supports_preserve_thinking = flags["supports_preserve_thinking"] + self._supports_tools = flags["supports_tools"] except Exception as e: logger.warning(f"Failed to read GGUF metadata: {e}") @@ -1523,7 +1615,8 @@ class LlamaCppBackend: # For reasoning models, set default thinking mode. # Qwen3.5/3.6 models below 9B (0.8B, 2B, 4B) disable thinking by default. # Only 9B and larger enable thinking. - if self._supports_reasoning: + # Always-on templates ignore the kwarg entirely, so skip. + if self._supports_reasoning and not self._reasoning_always_on: thinking_default = True mid = (model_identifier or "").lower() if "qwen3.5" in mid or "qwen3.6" in mid: @@ -1531,15 +1624,14 @@ class LlamaCppBackend: if size_val is not None and size_val < 9: thinking_default = False self._reasoning_default = thinking_default + reasoning_kw = self._reasoning_kwargs(thinking_default) cmd.extend( [ "--chat-template-kwargs", - json.dumps({"enable_thinking": thinking_default}), + json.dumps(reasoning_kw), ] ) - logger.info( - f"Reasoning model: enable_thinking={thinking_default} by default" - ) + logger.info(f"Reasoning model: {reasoning_kw} by default") if mmproj_path: if not Path(mmproj_path).is_file(): @@ -1767,6 +1859,9 @@ class LlamaCppBackend: self._chat_template = None self._supports_reasoning = False self._reasoning_always_on = False + self._reasoning_style = "enable_thinking" + self._reasoning_default = True + self._supports_preserve_thinking = False self._supports_tools = False self._cache_type_kv = None self._speculative_type = None @@ -2317,6 +2412,8 @@ class LlamaCppBackend: stop: Optional[list[str]] = None, cancel_event: Optional[threading.Event] = None, enable_thinking: Optional[bool] = None, + reasoning_effort: Optional[str] = None, + preserve_thinking: Optional[bool] = None, ) -> Generator[str | dict, None, None]: """ Send a chat completion request to llama-server and stream tokens back. @@ -2341,9 +2438,12 @@ class LlamaCppBackend: "repeat_penalty": repetition_penalty, "presence_penalty": presence_penalty, } - # Pass enable_thinking per-request for reasoning models - if self._supports_reasoning and enable_thinking is not None: - payload["chat_template_kwargs"] = {"enable_thinking": enable_thinking} + # Pass enable_thinking / reasoning_effort / preserve_thinking per-request + _reasoning_kw = self._request_reasoning_kwargs( + enable_thinking, reasoning_effort, preserve_thinking + ) + if _reasoning_kw is not None: + payload["chat_template_kwargs"] = _reasoning_kw if max_tokens is not None: payload["max_tokens"] = max_tokens if stop: @@ -2479,6 +2579,8 @@ class LlamaCppBackend: stop: Optional[list[str]] = None, cancel_event: Optional[threading.Event] = None, enable_thinking: Optional[bool] = None, + reasoning_effort: Optional[str] = None, + preserve_thinking: Optional[bool] = None, max_tool_iterations: int = 25, auto_heal_tool_calls: bool = True, tool_call_timeout: int = 300, @@ -2557,8 +2659,11 @@ class LlamaCppBackend: "tools": tools, "tool_choice": "auto", } - if self._supports_reasoning and enable_thinking is not None: - payload["chat_template_kwargs"] = {"enable_thinking": enable_thinking} + _reasoning_kw = self._request_reasoning_kwargs( + enable_thinking, reasoning_effort, preserve_thinking + ) + if _reasoning_kw is not None: + payload["chat_template_kwargs"] = _reasoning_kw if max_tokens is not None: payload["max_tokens"] = max_tokens if stop: @@ -3207,10 +3312,11 @@ class LlamaCppBackend: "repeat_penalty": repetition_penalty, "presence_penalty": presence_penalty, } - if self._supports_reasoning and enable_thinking is not None: - stream_payload["chat_template_kwargs"] = { - "enable_thinking": enable_thinking - } + _reasoning_kw = self._request_reasoning_kwargs( + enable_thinking, reasoning_effort, preserve_thinking + ) + if _reasoning_kw is not None: + stream_payload["chat_template_kwargs"] = _reasoning_kw if max_tokens is not None: stream_payload["max_tokens"] = max_tokens if stop: diff --git a/studio/backend/models/inference.py b/studio/backend/models/inference.py index 324002ddbf..e5b037755d 100644 --- a/studio/backend/models/inference.py +++ b/studio/backend/models/inference.py @@ -157,12 +157,20 @@ class LoadResponse(BaseModel): ) supports_reasoning: bool = Field( False, - description = "Whether model supports thinking/reasoning mode (enable_thinking)", + description = "Whether model supports thinking/reasoning mode (enable_thinking or reasoning_effort)", + ) + reasoning_style: Literal["enable_thinking", "reasoning_effort"] = Field( + "enable_thinking", + description = "Reasoning control style: 'enable_thinking' (boolean) or 'reasoning_effort' (low|medium|high)", ) reasoning_always_on: bool = Field( False, description = "Whether reasoning is always on (hardcoded tags, not toggleable)", ) + supports_preserve_thinking: bool = Field( + False, + description = "Whether the template understands the optional preserve_thinking kwarg (Qwen3.6-style)", + ) supports_tools: bool = Field( False, description = "Whether model supports tool calling (web search, etc.)", @@ -261,9 +269,17 @@ class InferenceStatusResponse(BaseModel): supports_reasoning: bool = Field( False, description = "Whether the active model supports reasoning/thinking mode" ) + reasoning_style: Literal["enable_thinking", "reasoning_effort"] = Field( + "enable_thinking", + description = "Reasoning control style: 'enable_thinking' (boolean) or 'reasoning_effort' (low|medium|high)", + ) reasoning_always_on: bool = Field( False, description = "Whether reasoning is always on (not toggleable)" ) + supports_preserve_thinking: bool = Field( + False, + description = "Whether the active model's template understands the optional preserve_thinking kwarg", + ) supports_tools: bool = Field( False, description = "Whether the active model supports tool calling" ) @@ -481,6 +497,14 @@ class ChatCompletionRequest(BaseModel): None, description = "[x-unsloth] Enable/disable thinking/reasoning mode for supported models", ) + reasoning_effort: Optional[Literal["low", "medium", "high"]] = Field( + None, + description = "[x-unsloth] Reasoning effort level ('low'|'medium'|'high') for Harmony-style reasoning models (e.g. gpt-oss). Overrides enable_thinking when the active model uses reasoning_effort style.", + ) + preserve_thinking: Optional[bool] = Field( + None, + description = "[x-unsloth] When true, keep historical blocks from past assistant turns in the prompt (Qwen3.6 templates). Independent of enable_thinking / reasoning_effort.", + ) enable_tools: Optional[bool] = Field( None, description = "[x-unsloth] Enable tool calling for supported models", diff --git a/studio/backend/routes/inference.py b/studio/backend/routes/inference.py index e17f8f5882..ed331a5660 100644 --- a/studio/backend/routes/inference.py +++ b/studio/backend/routes/inference.py @@ -113,7 +113,7 @@ if str(backend_path) not in sys.path: # Import backend functions try: from core.inference import get_inference_backend - from core.inference.llama_cpp import LlamaCppBackend + from core.inference.llama_cpp import LlamaCppBackend, detect_reasoning_flags from utils.models import ModelConfig from utils.inference import load_inference_config from utils.models.model_config import load_model_defaults @@ -122,7 +122,7 @@ except ImportError: if str(parent_backend) not in sys.path: sys.path.insert(0, str(parent_backend)) from core.inference import get_inference_backend - from core.inference.llama_cpp import LlamaCppBackend + from core.inference.llama_cpp import LlamaCppBackend, detect_reasoning_flags from utils.models import ModelConfig from utils.inference import load_inference_config from utils.models.model_config import load_model_defaults @@ -273,7 +273,9 @@ async def load_model( max_context_length = llama_backend.max_context_length, native_context_length = llama_backend.native_context_length, supports_reasoning = llama_backend.supports_reasoning, + reasoning_style = llama_backend.reasoning_style, reasoning_always_on = llama_backend.reasoning_always_on, + supports_preserve_thinking = llama_backend.supports_preserve_thinking, chat_template = llama_backend.chat_template, speculative_type = llama_backend.speculative_type, ) @@ -295,6 +297,21 @@ async def load_model( logger.warning( f"Could not retrieve chat template for {backend.active_model_name}: {e}" ) + # Non-GGUF: only advertise reasoning for gpt-oss Harmony, + # which emits reasoning via channels at the tokenizer level. + # Template-level chat_template_kwargs (enable_thinking / + # preserve_thinking / tools) are not yet forwarded through + # the transformers generation path, so avoid advertising + # controls the server cannot honour outside GGUF. + _sf_supports_reasoning = False + _sf_reasoning_style = "enable_thinking" + if hasattr(backend, "_is_gpt_oss_model"): + try: + if backend._is_gpt_oss_model(): + _sf_supports_reasoning = True + _sf_reasoning_style = "reasoning_effort" + except Exception: + pass return LoadResponse( status = "already_loaded", model = backend.active_model_name, @@ -309,6 +326,11 @@ async def load_model( requires_trust_remote_code = bool( inference_config.get("trust_remote_code", False) ), + supports_reasoning = _sf_supports_reasoning, + reasoning_style = _sf_reasoning_style, + reasoning_always_on = False, + supports_preserve_thinking = False, + supports_tools = False, chat_template = _chat_template, ) @@ -422,7 +444,9 @@ async def load_model( max_context_length = llama_backend.max_context_length, native_context_length = llama_backend.native_context_length, supports_reasoning = llama_backend.supports_reasoning, + reasoning_style = llama_backend.reasoning_style, reasoning_always_on = llama_backend.reasoning_always_on, + supports_preserve_thinking = llama_backend.supports_preserve_thinking, supports_tools = llama_backend.supports_tools, cache_type_kv = llama_backend.cache_type_kv, chat_template = llama_backend.chat_template, @@ -545,6 +569,20 @@ async def load_model( except Exception: pass + # Non-GGUF: gpt-oss Harmony surfaces reasoning via tokenizer-level + # channels; other safetensors reasoning/tools/preserve-thinking + # knobs are not forwarded to tokenizer.apply_chat_template yet, so + # we only advertise support for the Harmony case here. + _sf_supports_reasoning = False + _sf_reasoning_style = "enable_thinking" + if hasattr(backend, "_is_gpt_oss_model"): + try: + if backend._is_gpt_oss_model(): + _sf_supports_reasoning = True + _sf_reasoning_style = "reasoning_effort" + except Exception: + pass + return LoadResponse( status = "loaded", model = config.identifier, @@ -559,6 +597,11 @@ async def load_model( requires_trust_remote_code = bool( inference_config.get("trust_remote_code", False) ), + supports_reasoning = _sf_supports_reasoning, + reasoning_style = _sf_reasoning_style, + reasoning_always_on = False, + supports_preserve_thinking = False, + supports_tools = False, chat_template = _chat_template, ) @@ -766,7 +809,9 @@ async def get_status( (_inference_cfg or {}).get("trust_remote_code", False) ), supports_reasoning = llama_backend.supports_reasoning, + reasoning_style = llama_backend.reasoning_style, reasoning_always_on = llama_backend.reasoning_always_on, + supports_preserve_thinking = llama_backend.supports_preserve_thinking, supports_tools = llama_backend.supports_tools, context_length = llama_backend.context_length, max_context_length = llama_backend.max_context_length, @@ -788,10 +833,18 @@ async def get_status( audio_type = model_info.get("audio_type") has_audio_input = model_info.get("has_audio_input", False) - # gpt-oss safetensors models support reasoning via harmony channels + # Non-GGUF: only gpt-oss Harmony is wired through the transformers + # generation path. Other template-level reasoning / tool kwargs + # are not yet forwarded, so we do not advertise them here. supports_reasoning = False + reasoning_style = "enable_thinking" if backend.active_model_name and hasattr(backend, "_is_gpt_oss_model"): - supports_reasoning = backend._is_gpt_oss_model() + try: + if backend._is_gpt_oss_model(): + supports_reasoning = True + reasoning_style = "reasoning_effort" + except Exception: + pass inference_config = ( load_inference_config(backend.active_model_name) if backend.active_model_name @@ -812,6 +865,10 @@ async def get_status( (inference_config or {}).get("trust_remote_code", False) ), supports_reasoning = supports_reasoning, + reasoning_style = reasoning_style, + reasoning_always_on = False, + supports_preserve_thinking = False, + supports_tools = False, ) except Exception as e: @@ -1393,6 +1450,8 @@ async def openai_chat_completions( presence_penalty = payload.presence_penalty, cancel_event = cancel_event, enable_thinking = payload.enable_thinking, + reasoning_effort = payload.reasoning_effort, + preserve_thinking = payload.preserve_thinking, auto_heal_tool_calls = payload.auto_heal_tool_calls if payload.auto_heal_tool_calls is not None else True, @@ -1562,6 +1621,8 @@ async def openai_chat_completions( presence_penalty = payload.presence_penalty, cancel_event = cancel_event, enable_thinking = payload.enable_thinking, + reasoning_effort = payload.reasoning_effort, + preserve_thinking = payload.preserve_thinking, ) _gguf_sentinel = object() diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx index 0b97d98dfd..3f67b11b51 100644 --- a/studio/frontend/src/components/assistant-ui/thread.tsx +++ b/studio/frontend/src/components/assistant-ui/thread.tsx @@ -24,8 +24,15 @@ import { useScrollThreadToBottom, } from "@/components/assistant-ui/use-intent-aware-autoscroll"; import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { sentAudioNames } from "@/features/chat/api/chat-adapter"; import { useChatRuntimeStore } from "@/features/chat/stores/chat-runtime-store"; +import { applyQwenThinkingParams } from "@/features/chat/utils/qwen-params"; import { deleteThreadMessage } from "@/features/chat/utils/delete-thread-message"; import { AUDIO_ACCEPT, MAX_AUDIO_SIZE, fileToBase64 } from "@/lib/audio-utils"; import { copyToClipboard } from "@/lib/copy-to-clipboard"; @@ -373,18 +380,6 @@ const ComposerAudioUpload: FC = () => { ); }; -/** Qwen3/3.5 recommended params differ between thinking on/off. */ -function applyQwenThinkingParams(thinkingOn: boolean): void { - const store = useChatRuntimeStore.getState(); - const checkpoint = store.params.checkpoint?.toLowerCase() ?? ""; - if (!checkpoint.includes("qwen3")) { - return; - } - const params = thinkingOn - ? { temperature: 0.6, topP: 0.95, topK: 20, minP: 0.0 } - : { temperature: 0.7, topP: 0.8, topK: 20, minP: 0.0 }; - store.setParams({ ...store.params, ...params }); -} const ReasoningToggle: FC = () => { const modelLoaded = useChatRuntimeStore( @@ -393,8 +388,49 @@ const ReasoningToggle: FC = () => { const supportsReasoning = useChatRuntimeStore((s) => s.supportsReasoning); const reasoningEnabled = useChatRuntimeStore((s) => s.reasoningEnabled); const setReasoningEnabled = useChatRuntimeStore((s) => s.setReasoningEnabled); + const reasoningStyle = useChatRuntimeStore((s) => s.reasoningStyle); + const reasoningEffort = useChatRuntimeStore((s) => s.reasoningEffort); + const setReasoningEffort = useChatRuntimeStore((s) => s.setReasoningEffort); const disabled = !(modelLoaded && supportsReasoning); + if (reasoningStyle === "reasoning_effort") { + return ( + + + + + + {(["low", "medium", "high"] as const).map((level) => ( + setReasoningEffort(level)} + > + {level.charAt(0).toUpperCase() + level.slice(1)} + {reasoningEffort === level ? " \u2713" : ""} + + ))} + + + ); + } + return ( + ); +}; + const WebSearchToggle: FC = () => { const modelLoaded = useChatRuntimeStore( (s) => !!s.params.checkpoint && !s.modelLoading, @@ -551,6 +625,7 @@ const ComposerAction: FC<{ disabled?: boolean }> = ({ disabled }) => { + diff --git a/studio/frontend/src/features/chat/api/chat-adapter.ts b/studio/frontend/src/features/chat/api/chat-adapter.ts index 53a935cf45..a93120397e 100644 --- a/studio/frontend/src/features/chat/api/chat-adapter.ts +++ b/studio/frontend/src/features/chat/api/chat-adapter.ts @@ -385,6 +385,8 @@ async function autoLoadSmallestModel(): Promise<{ supportsReasoning: loadResp.supports_reasoning ?? false, reasoningAlwaysOn: loadResp.reasoning_always_on ?? false, reasoningEnabled: loadResp.supports_reasoning ?? false, + reasoningStyle: loadResp.reasoning_style ?? "enable_thinking", + supportsPreserveThinking: loadResp.supports_preserve_thinking ?? false, supportsTools: loadResp.supports_tools ?? false, toolsEnabled: loadResp.supports_tools ?? false, codeToolsEnabled: loadResp.supports_tools ?? false, @@ -433,6 +435,14 @@ async function autoLoadSmallestModel(): Promise<{ sfLoadResp.requires_trust_remote_code ?? false, ); store.setParams({ ...store.params, maxTokens: 4096 }); + useChatRuntimeStore.setState({ + supportsReasoning: sfLoadResp.supports_reasoning ?? false, + reasoningAlwaysOn: sfLoadResp.reasoning_always_on ?? false, + reasoningEnabled: sfLoadResp.supports_reasoning ?? false, + reasoningStyle: sfLoadResp.reasoning_style ?? "enable_thinking", + supportsPreserveThinking: sfLoadResp.supports_preserve_thinking ?? false, + supportsTools: sfLoadResp.supports_tools ?? false, + }); const sfModel: ChatModelSummary = { id: repo.repo_id, name: sfLoadResp.display_name ?? repo.repo_id, @@ -501,6 +511,8 @@ async function autoLoadSmallestModel(): Promise<{ supportsReasoning: loadResp.supports_reasoning ?? false, reasoningAlwaysOn: loadResp.reasoning_always_on ?? false, reasoningEnabled: loadResp.supports_reasoning ?? false, + reasoningStyle: loadResp.reasoning_style ?? "enable_thinking", + supportsPreserveThinking: loadResp.supports_preserve_thinking ?? false, supportsTools: loadResp.supports_tools ?? false, toolsEnabled: loadResp.supports_tools ?? false, codeToolsEnabled: loadResp.supports_tools ?? false, @@ -696,7 +708,14 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter { let serverMetadata: { usage?: ServerUsage; timings?: ServerTimings } | null = null; try { - const { supportsReasoning, reasoningEnabled } = runtime; + const { + supportsReasoning, + reasoningEnabled, + reasoningStyle, + reasoningEffort, + supportsPreserveThinking, + preserveThinking, + } = runtime; const stream = streamChatCompletions( { model: params.checkpoint, @@ -712,7 +731,12 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter { image_base64: imageBase64, audio_base64: audioBase64, ...(useAdapter === undefined ? {} : { use_adapter: useAdapter }), - ...(supportsReasoning ? { enable_thinking: reasoningEnabled } : {}), + ...(supportsReasoning + ? reasoningStyle === "reasoning_effort" + ? { reasoning_effort: reasoningEffort } + : { enable_thinking: reasoningEnabled } + : {}), + ...(supportsPreserveThinking ? { preserve_thinking: preserveThinking } : {}), ...(supportsTools && (toolsEnabled || codeToolsEnabled) ? { enable_tools: true, diff --git a/studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts b/studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts index 6c870d74a9..c9dcd911a2 100644 --- a/studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts +++ b/studio/frontend/src/features/chat/hooks/use-chat-model-runtime.ts @@ -253,7 +253,9 @@ export function useChatModelRuntime() { max_context_length: statusRes.max_context_length, native_context_length: statusRes.native_context_length, supports_reasoning: statusRes.supports_reasoning, + reasoning_style: statusRes.reasoning_style, reasoning_always_on: statusRes.reasoning_always_on, + supports_preserve_thinking: statusRes.supports_preserve_thinking, supports_tools: statusRes.supports_tools, speculative_type: statusRes.speculative_type, }; @@ -265,6 +267,8 @@ export function useChatModelRuntime() { // Restore reasoning/tools support flags and context length const supportsReasoning = statusRes.supports_reasoning ?? false; const reasoningAlwaysOn = statusRes.reasoning_always_on ?? false; + const reasoningStyle = statusRes.reasoning_style ?? "enable_thinking"; + const supportsPreserveThinking = statusRes.supports_preserve_thinking ?? false; const supportsTools = statusRes.supports_tools ?? false; const currentGgufContextLength = statusRes.is_gguf ? (statusRes.context_length ?? null) @@ -279,7 +283,14 @@ export function useChatModelRuntime() { useChatRuntimeStore.setState({ supportsReasoning, reasoningAlwaysOn, + reasoningStyle, + supportsPreserveThinking, supportsTools, + // Reset per-turn reasoning flag so models that do not support + // reasoning do not inherit a stale off state from a prior model. + reasoningEnabled: supportsReasoning + ? useChatRuntimeStore.getState().reasoningEnabled + : true, ggufContextLength: currentGgufContextLength, ggufMaxContextLength, ggufNativeContextLength, @@ -498,6 +509,8 @@ export function useChatModelRuntime() { supportsReasoning: loadResponse.supports_reasoning ?? false, reasoningAlwaysOn, reasoningEnabled: reasoningAlwaysOn ? true : reasoningDefault, + reasoningStyle: loadResponse.reasoning_style ?? "enable_thinking", + supportsPreserveThinking: loadResponse.supports_preserve_thinking ?? false, supportsTools: loadResponse.supports_tools ?? false, toolsEnabled: loadResponse.supports_tools ?? false, codeToolsEnabled: loadResponse.supports_tools ?? false, diff --git a/studio/frontend/src/features/chat/shared-composer.tsx b/studio/frontend/src/features/chat/shared-composer.tsx index e133669a1c..3ba2f8b6f5 100644 --- a/studio/frontend/src/features/chat/shared-composer.tsx +++ b/studio/frontend/src/features/chat/shared-composer.tsx @@ -4,6 +4,13 @@ import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button"; import { CodeToggleIcon } from "@/components/assistant-ui/code-toggle-icon"; import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { applyQwenThinkingParams } from "@/features/chat/utils/qwen-params"; import { AUDIO_ACCEPT, MAX_AUDIO_SIZE, fileToBase64 } from "@/lib/audio-utils"; import { useAui } from "@assistant-ui/react"; import { cn } from "@/lib/utils"; @@ -245,6 +252,12 @@ export function SharedComposer({ const reasoningAlwaysOn = useChatRuntimeStore((s) => s.reasoningAlwaysOn); const reasoningEnabled = useChatRuntimeStore((s) => s.reasoningEnabled); const setReasoningEnabled = useChatRuntimeStore((s) => s.setReasoningEnabled); + const reasoningStyle = useChatRuntimeStore((s) => s.reasoningStyle); + const reasoningEffort = useChatRuntimeStore((s) => s.reasoningEffort); + const setReasoningEffort = useChatRuntimeStore((s) => s.setReasoningEffort); + const supportsPreserveThinking = useChatRuntimeStore((s) => s.supportsPreserveThinking); + const preserveThinking = useChatRuntimeStore((s) => s.preserveThinking); + const setPreserveThinking = useChatRuntimeStore((s) => s.setPreserveThinking); const supportsTools = useChatRuntimeStore((s) => s.supportsTools); const toolsEnabled = useChatRuntimeStore((s) => s.toolsEnabled); const setToolsEnabled = useChatRuntimeStore((s) => s.setToolsEnabled); @@ -391,6 +404,13 @@ export function SharedComposer({ store.setModelRequiresTrustRemoteCode( resp.requires_trust_remote_code ?? false, ); + useChatRuntimeStore.setState({ + supportsReasoning: resp.supports_reasoning ?? false, + reasoningAlwaysOn: resp.reasoning_always_on ?? false, + reasoningStyle: resp.reasoning_style ?? "enable_thinking", + supportsPreserveThinking: resp.supports_preserve_thinking ?? false, + supportsTools: resp.supports_tools ?? false, + }); return resp.status; } @@ -566,41 +586,93 @@ export function SharedComposer({ )} - + + + {(["low", "medium", "high"] as const).map((level) => ( + setReasoningEffort(level)} + > + {level.charAt(0).toUpperCase() + level.slice(1)} + {reasoningEffort === level ? " \u2713" : ""} + + ))} + + + ) : ( + + )} + {supportsPreserveThinking && ( + + > + {preserveThinking && modelLoaded ? ( + + ) : ( + + )} + Preserve Thinking + + )}