From c3276df7272f81fb305846c6ce57ba0e55fec55b Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 6 May 2026 15:32:28 +0200 Subject: [PATCH 01/21] Prepared test release v26.5.1 (#750) --- app/.idea/.idea.MindWork AI Studio/.idea/indexLayout.xml | 4 +++- app/MindWork AI Studio/Components/Changelog.Logs.cs | 1 + app/MindWork AI Studio/wwwroot/changelog/v26.5.1.md | 2 ++ app/MindWork AI Studio/wwwroot/changelog/v26.5.2.md | 1 + metadata.txt | 8 ++++---- runtime/Cargo.lock | 2 +- runtime/Cargo.toml | 2 +- runtime/tauri.conf.json | 4 ++-- 8 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 app/MindWork AI Studio/wwwroot/changelog/v26.5.1.md create mode 100644 app/MindWork AI Studio/wwwroot/changelog/v26.5.2.md diff --git a/app/.idea/.idea.MindWork AI Studio/.idea/indexLayout.xml b/app/.idea/.idea.MindWork AI Studio/.idea/indexLayout.xml index 7b08163c..30a3b985 100644 --- a/app/.idea/.idea.MindWork AI Studio/.idea/indexLayout.xml +++ b/app/.idea/.idea.MindWork AI Studio/.idea/indexLayout.xml @@ -1,7 +1,9 @@ - + + ../../mindwork-ai-studio + diff --git a/app/MindWork AI Studio/Components/Changelog.Logs.cs b/app/MindWork AI Studio/Components/Changelog.Logs.cs index 02d21b52..2772f02f 100644 --- a/app/MindWork AI Studio/Components/Changelog.Logs.cs +++ b/app/MindWork AI Studio/Components/Changelog.Logs.cs @@ -13,6 +13,7 @@ public partial class Changelog public static readonly Log[] LOGS = [ + new (236, "v26.5.1, build 236 (2026-05-06 13:06 UTC)", "v26.5.1.md"), new (235, "v26.4.1, build 235 (2026-04-17 17:25 UTC)", "v26.4.1.md"), new (234, "v26.2.2, build 234 (2026-02-22 14:16 UTC)", "v26.2.2.md"), new (233, "v26.2.1, build 233 (2026-02-01 19:16 UTC)", "v26.2.1.md"), diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.1.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.1.md new file mode 100644 index 00000000..05ccf49c --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.1.md @@ -0,0 +1,2 @@ +# v26.5.1, build 236 (2026-05-06 13:06 UTC) +- Changed the preview update path for a controlled prerelease test. Please do not install this prerelease manually. Production versions such as v26.4.1 will ignore this update. We are using this prerelease to test the clean update path for the migration from the Tauri v1 framework to the Tauri v2 framework. After a successful test, this prerelease will be removed. \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.2.md new file mode 100644 index 00000000..d06e40e8 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.2.md @@ -0,0 +1 @@ +# v26.5.2, build 237 (2026-05-xx xx:xx UTC) diff --git a/metadata.txt b/metadata.txt index d6cfb34a..7c5400f6 100644 --- a/metadata.txt +++ b/metadata.txt @@ -1,12 +1,12 @@ -26.4.1 -2026-04-17 17:25:44 UTC -235 +26.5.1 +2026-05-06 13:06:02 UTC +236 9.0.116 (commit fb4af7e1b3) 9.0.15 (commit 4250c8399a) 1.93.1 (commit 01f6ddf75) 8.15.0 1.8.3 -c6ed7e3c0ce, release +ece329140e4, release osx-arm64 144.0.7543.0 1.17.1 \ No newline at end of file diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index 2a943f1d..f838558f 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -2770,7 +2770,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mindwork-ai-studio" -version = "26.4.1" +version = "26.5.1" dependencies = [ "aes", "arboard", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index ff7cfcc2..0d7739ab 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mindwork-ai-studio" -version = "26.4.1" +version = "26.5.1" edition = "2021" description = "MindWork AI Studio" authors = ["Thorsten Sommer"] diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index 40f4cfbd..46a7c4f2 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -6,7 +6,7 @@ }, "package": { "productName": "MindWork AI Studio", - "version": "26.4.1" + "version": "26.5.1" }, "tauri": { "allowlist": { @@ -84,7 +84,7 @@ "updater": { "active": true, "endpoints": [ - "https://github.com/MindWorkAI/AI-Studio/releases/latest/download/latest.json" + "https://github.com/MindWorkAI/AI-Studio/releases/download/v26.5.2/latest.json" ], "dialog": false, "windows": { From 6ee5a1945bdde938ebaef5ef43198696a246655e Mon Sep 17 00:00:00 2001 From: Paul Koudelka <106623909+PaulKoudelka@users.noreply.github.com> Date: Wed, 6 May 2026 18:45:50 +0200 Subject: [PATCH 02/21] Upgrade to Tauri v2 (#693) Co-authored-by: Thorsten Sommer --- .github/workflows/build-and-release.yml | 39 +- .gitignore | 3 + app/Build/Commands/UpdateMetadataCommands.cs | 2 +- .../Components/Changelog.Logs.cs | 1 + .../wwwroot/changelog/v26.5.2.md | 3 +- .../wwwroot/changelog/v26.5.3.md | 1 + metadata.txt | 12 +- runtime/Cargo.lock | 3043 ++++++++++------- runtime/Cargo.toml | 23 +- runtime/build.rs | 27 +- runtime/capabilities/default.json | 34 + runtime/src/app_window.rs | 647 ++-- runtime/src/dotnet.rs | 30 +- runtime/src/file_actions.rs | 298 ++ runtime/src/lib.rs | 3 +- runtime/src/qdrant.rs | 26 +- runtime/src/runtime_api.rs | 8 +- runtime/tauri.conf.json | 128 +- 18 files changed, 2599 insertions(+), 1729 deletions(-) create mode 100644 app/MindWork AI Studio/wwwroot/changelog/v26.5.3.md create mode 100644 runtime/capabilities/default.json create mode 100644 runtime/src/file_actions.rs diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 60b4b947..00e20baa 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -704,30 +704,31 @@ jobs: if: matrix.platform == 'ubuntu-22.04' && contains(matrix.rust_target, 'x86_64') run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf libfuse2 + sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 - name: Setup dependencies (Ubuntu-specific, ARM) if: matrix.platform == 'ubuntu-22.04-arm' && contains(matrix.rust_target, 'aarch64') run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf libfuse2 + sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 - name: Setup Tauri (Unix) if: matrix.platform != 'windows-latest' run: | - if ! cargo tauri --version > /dev/null 2>&1; then - cargo install --version 1.6.2 tauri-cli + if ! cargo tauri --version 2>/dev/null | grep -Eq '^tauri-cli 2\.'; then + cargo install tauri-cli --version "^2.0.0" --locked --force else - echo "Tauri is already installed" + echo "Tauri CLI v2 is already installed" fi - name: Setup Tauri (Windows) if: matrix.platform == 'windows-latest' run: | - if (-not (cargo tauri --version 2>$null)) { - cargo install --version 1.6.2 tauri-cli + $tauriVersion = cargo tauri --version 2>$null + if (-not $tauriVersion -or $tauriVersion -notmatch '^tauri-cli 2\.') { + cargo install tauri-cli --version "^2.0.0" --locked --force } else { - Write-Output "Tauri is already installed" + Write-Output "Tauri CLI v2 is already installed" } - name: Delete previous artifact, which may exist due to caching (macOS) @@ -771,8 +772,8 @@ jobs: echo "Running PR test build without updater bundle signing" bundles="${{ matrix.tauri_bundle_pr }}" else - export TAURI_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY" - export TAURI_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD" + export TAURI_SIGNING_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY" + export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD" fi cd runtime @@ -790,8 +791,8 @@ jobs: Write-Output "Running PR test build without updater bundle signing" $bundles = "${{ matrix.tauri_bundle_pr }}" } else { - $env:TAURI_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY" - $env:TAURI_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD" + $env:TAURI_SIGNING_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY" + $env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD" } cd runtime @@ -883,14 +884,14 @@ jobs: # Find and process files in the artifacts directory: find "$GITHUB_WORKSPACE/artifacts" -type f | while read -r FILE; do - if [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz" ]]; then - TARGET_NAME="MindWork AI Studio_x64.app.tar.gz" - elif [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz.sig" ]]; then + if [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz.sig" ]]; then TARGET_NAME="MindWork AI Studio_x64.app.tar.gz.sig" - elif [[ "$FILE" == *"osx-arm64"* && "$FILE" == *".tar.gz" ]]; then - TARGET_NAME="MindWork AI Studio_aarch64.app.tar.gz" + elif [[ "$FILE" == *"osx-x64"* && "$FILE" == *".tar.gz" ]]; then + TARGET_NAME="MindWork AI Studio_x64.app.tar.gz" elif [[ "$FILE" == *"osx-arm64"* && "$FILE" == *".tar.gz.sig" ]]; then TARGET_NAME="MindWork AI Studio_aarch64.app.tar.gz.sig" + elif [[ "$FILE" == *"osx-arm64"* && "$FILE" == *".tar.gz" ]]; then + TARGET_NAME="MindWork AI Studio_aarch64.app.tar.gz" else TARGET_NAME="$(basename "$FILE")" TARGET_NAME=$(echo "$TARGET_NAME" | sed "s/_${VERSION}//") @@ -941,9 +942,9 @@ jobs: platform="linux-x86_64" elif [[ "$sig_file" == *"aarch64.AppImage"* ]]; then platform="linux-aarch64" - elif [[ "$sig_file" == *"x64-setup.nsis"* ]]; then + elif [[ "$sig_file" == *"x64-setup"* ]]; then platform="windows-x86_64" - elif [[ "$sig_file" == *"arm64-setup.nsis"* ]]; then + elif [[ "$sig_file" == *"arm64-setup"* ]]; then platform="windows-aarch64" else echo "Platform not recognized: '$sig_file'" diff --git a/.gitignore b/.gitignore index 3175fdb1..6c081ead 100644 --- a/.gitignore +++ b/.gitignore @@ -169,3 +169,6 @@ orleans.codegen.cs # Ignore GitHub Copilot migration files: **/copilot.data.migration.*.xml + +# Tauri generated schemas/manifests +/runtime/gen/ diff --git a/app/Build/Commands/UpdateMetadataCommands.cs b/app/Build/Commands/UpdateMetadataCommands.cs index 5ec929ab..f3b0799e 100644 --- a/app/Build/Commands/UpdateMetadataCommands.cs +++ b/app/Build/Commands/UpdateMetadataCommands.cs @@ -245,7 +245,7 @@ public sealed partial class UpdateMetadataCommands Console.WriteLine("- Start building the Rust runtime ..."); var pathRuntime = Environment.GetRustRuntimeDirectory(); - var rustBuildOutput = await this.ReadCommandOutput(pathRuntime, "cargo", "tauri build --bundles none", true); + var rustBuildOutput = await this.ReadCommandOutput(pathRuntime, "cargo", "tauri build --no-bundle", true); var rustBuildOutputLines = rustBuildOutput.Split([global::System.Environment.NewLine], StringSplitOptions.RemoveEmptyEntries); var foundRustIssue = false; foreach (var buildOutputLine in rustBuildOutputLines) diff --git a/app/MindWork AI Studio/Components/Changelog.Logs.cs b/app/MindWork AI Studio/Components/Changelog.Logs.cs index 2772f02f..714a61e8 100644 --- a/app/MindWork AI Studio/Components/Changelog.Logs.cs +++ b/app/MindWork AI Studio/Components/Changelog.Logs.cs @@ -13,6 +13,7 @@ public partial class Changelog public static readonly Log[] LOGS = [ + new (237, "v26.5.2, build 237 (2026-05-06 16:38 UTC)", "v26.5.2.md"), new (236, "v26.5.1, build 236 (2026-05-06 13:06 UTC)", "v26.5.1.md"), new (235, "v26.4.1, build 235 (2026-04-17 17:25 UTC)", "v26.4.1.md"), new (234, "v26.2.2, build 234 (2026-02-22 14:16 UTC)", "v26.2.2.md"), diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.2.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.2.md index d06e40e8..2c814910 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.5.2.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.2.md @@ -1 +1,2 @@ -# v26.5.2, build 237 (2026-05-xx xx:xx UTC) +# v26.5.2, build 237 (2026-05-06 16:38 UTC) +- Updated the underlying Tauri framework from version 1 to the latest version 2. Please do not install this prerelease manually. Production versions such as v26.4.1 will ignore this update. We are using this prerelease to test the clean update path for the migration from the Tauri v1 framework to the Tauri v2 framework. After a successful test, this prerelease will be removed. diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.3.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.3.md new file mode 100644 index 00000000..ff2b7c29 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.3.md @@ -0,0 +1 @@ +# v26.5.3, build 238 (2026-05-xx xx:xx UTC) diff --git a/metadata.txt b/metadata.txt index 7c5400f6..bea38b51 100644 --- a/metadata.txt +++ b/metadata.txt @@ -1,12 +1,12 @@ -26.5.1 -2026-05-06 13:06:02 UTC -236 +26.5.2 +2026-05-06 16:38:01 UTC +237 9.0.116 (commit fb4af7e1b3) 9.0.15 (commit 4250c8399a) -1.93.1 (commit 01f6ddf75) +1.95.0 (commit 59807616e) 8.15.0 -1.8.3 -ece329140e4, release +2.11.1 +bcf15e91881, release osx-arm64 144.0.7543.0 1.17.1 \ No newline at end of file diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index f838558f..6d03ec12 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -88,11 +88,11 @@ dependencies = [ "clipboard-win", "image 0.25.2", "log", - "objc2", + "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation", + "objc2-foundation 0.3.0", "parking_lot", "percent-encoding", "windows-sys 0.60.2", @@ -138,6 +138,120 @@ dependencies = [ "syn 2.0.117", ] +[[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 1.1.4", + "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 1.1.4", +] + +[[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.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -160,6 +274,12 @@ dependencies = [ "syn 2.0.117", ] +[[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.81" @@ -173,26 +293,25 @@ dependencies = [ [[package]] name = "atk" -version = "0.15.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" dependencies = [ "atk-sys", - "bitflags 1.3.2", "glib", "libc", ] [[package]] name = "atk-sys" -version = "0.15.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58aeb089fb698e06db8089971c7ee317ab9644bade33383f63631437b03aafb6" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.2.2", + "system-deps", ] [[package]] @@ -253,12 +372,6 @@ dependencies = [ "fs_extra", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.7" @@ -278,14 +391,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" [[package]] -name = "bincode" -version = "1.3.3" +name = "bit-set" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "serde", + "bit-vec", ] +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bit_field" version = "0.10.2" @@ -303,12 +422,9 @@ name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" - -[[package]] -name = "block" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" +dependencies = [ + "serde", +] [[package]] name = "block-buffer" @@ -329,10 +445,41 @@ dependencies = [ ] [[package]] -name = "brotli" -version = "7.0.0" +name = "block2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc97b8f16f944bba54f0433f07e30be199b6dc2bd25937444bbad560bcea29bd" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[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", @@ -341,24 +488,14 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "4.0.1" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", ] -[[package]] -name = "bstr" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "bumpalo" version = "3.16.0" @@ -413,26 +550,27 @@ dependencies = [ [[package]] name = "cairo-rs" -version = "0.15.12" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "cairo-sys-rs", "glib", "libc", + "once_cell", "thiserror 1.0.63", ] [[package]] name = "cairo-sys-rs" -version = "0.15.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c55d429bef56ac9172d25fecb85dc8068307d17acd74b377866b7a1ef25d3c8" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" dependencies = [ "glib-sys", "libc", - "system-deps 6.2.2", + "system-deps", ] [[package]] @@ -453,13 +591,45 @@ dependencies = [ ] [[package]] -name = "cargo_toml" -version = "0.15.3" +name = "camino" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "599aa35200ffff8f04c1925aa1acc92fa2e08874379ef42e210a80e527e60838" +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", - "toml 0.7.8", +] + +[[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.12", +] + +[[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]] @@ -500,15 +670,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "cfg-expr" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3431df59f28accaf4cb4eed4a9acc66bea3f3c3753aa6cdc2f024174ef232af7" -dependencies = [ - "smallvec", -] - [[package]] name = "cfg-expr" version = "0.15.8" @@ -585,36 +746,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cocoa" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" -dependencies = [ - "bitflags 1.3.2", - "block", - "cocoa-foundation", - "core-foundation 0.9.4", - "core-graphics", - "foreign-types", - "libc", - "objc", -] - -[[package]] -name = "cocoa-foundation" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" -dependencies = [ - "bitflags 1.3.2", - "block", - "core-foundation 0.9.4", - "core-graphics-types", - "libc", - "objc", -] - [[package]] name = "codepage" version = "0.1.2" @@ -640,6 +771,15 @@ dependencies = [ "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 = "console_error_panic_hook" version = "0.1.7" @@ -666,12 +806,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" -[[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" @@ -711,25 +845,38 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" -version = "0.22.3" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", + "bitflags 2.6.0", + "core-foundation 0.10.0", "core-graphics-types", - "foreign-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", + "core-graphics-types", + "foreign-types 0.5.0", "libc", ] [[package]] name = "core-graphics-types" -version = "0.1.3" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", + "bitflags 2.6.0", + "core-foundation 0.10.0", "libc", ] @@ -827,19 +974,15 @@ dependencies = [ [[package]] name = "cssparser" -version = "0.27.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "754b69d351cdc2d8ee09ae203db831e005560fc6030da058f86ad60c92a9cb0a" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" dependencies = [ "cssparser-macros", "dtoa-short", - "itoa 0.4.8", - "matches", - "phf 0.8.0", - "proc-macro2", - "quote", + "itoa", + "phf", "smallvec", - "syn 1.0.109", ] [[package]] @@ -854,14 +997,20 @@ dependencies = [ [[package]] name = "ctor" -version = "0.2.8" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb49164822f3ee45b17acd4a208cfc1251410cf0cad9a833234c9890774dd9f" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" dependencies = [ - "quote", - "syn 2.0.117", + "ctor-proc-macro", + "dtor", ] +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + [[package]] name = "darling" version = "0.20.10" @@ -976,11 +1125,19 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.18" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +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 = [ - "convert_case", "proc-macro2", "quote", "rustc_version", @@ -1032,32 +1189,26 @@ dependencies = [ ] [[package]] -name = "dirs-next" -version = "2.0.0" +name = "dirs" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "cfg-if", - "dirs-sys-next", + "dirs-sys", ] [[package]] -name = "dirs-sys-next" -version = "0.1.2" +name = "dirs-sys" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", + "option-ext", "redox_users", - "winapi", + "windows-sys 0.61.2", ] -[[package]] -name = "dispatch" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" - [[package]] name = "dispatch2" version = "0.3.0" @@ -1065,7 +1216,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ "bitflags 2.6.0", - "objc2", + "block2 0.6.2", + "libc", + "objc2 0.6.4", ] [[package]] @@ -1079,6 +1232,53 @@ dependencies = [ "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", + "foldhash 0.2.0", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[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.9" @@ -1094,12 +1294,33 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[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 = "either" version = "1.13.0" @@ -1108,16 +1329,16 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "embed-resource" -version = "2.4.3" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4edcacde9351c33139a41e3c97eb2334351a81a2791bebb0b243df837128f602" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.8.16", + "toml 1.1.2+spec-1.1.0", "vswhom", - "winreg 0.52.0", + "winreg", ] [[package]] @@ -1135,12 +1356,50 @@ 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.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[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" @@ -1157,6 +1416,27 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0474425d51df81997e2f90a21591180b38eccf27292d755f3e30750225c175b" +[[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 = "exr" version = "1.73.0" @@ -1212,16 +1492,16 @@ dependencies = [ "atomic 0.6.0", "pear", "serde", - "toml 0.8.16", + "toml 0.8.2", "uncased", "version_check", ] [[package]] name = "file-format" -version = "0.28.0" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eab8aa2fba5f39f494000a22f44bf3c755b7d7f8ffad3f36c6d507893074159" +checksum = "55d9ccda37e95b4f0978a3074b4a9939979103a7256459cfb449c9c84d1adf23" [[package]] name = "filetime" @@ -1265,15 +1545,6 @@ dependencies = [ "thiserror 2.0.12", ] -[[package]] -name = "fluent-uri" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "fnv" version = "1.0.7" @@ -1286,13 +1557,40 @@ 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", + "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]] @@ -1301,6 +1599,12 @@ 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" @@ -1316,16 +1620,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" -[[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" version = "0.3.32" @@ -1374,6 +1668,19 @@ 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" @@ -1414,22 +1721,12 @@ dependencies = [ "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.15.4" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e05c1f572ab0e1f15be94217f0dc29088c248b14f792a5ff0af0d84bcda9e8" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" dependencies = [ - "bitflags 1.3.2", "cairo-rs", "gdk-pixbuf", "gdk-sys", @@ -1441,35 +1738,35 @@ dependencies = [ [[package]] name = "gdk-pixbuf" -version = "0.15.11" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad38dd9cc8b099cceecdf41375bb6d481b1b5a7cd5cd603e10a69a9383f8619a" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" dependencies = [ - "bitflags 1.3.2", "gdk-pixbuf-sys", "gio", "glib", "libc", + "once_cell", ] [[package]] name = "gdk-pixbuf-sys" -version = "0.15.10" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "140b2f5378256527150350a8346dbdb08fadc13453a7a2d73aecd5fab3c402a7" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" dependencies = [ "gio-sys", "glib-sys", "gobject-sys", "libc", - "system-deps 6.2.2", + "system-deps", ] [[package]] name = "gdk-sys" -version = "0.15.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e7a08c1e8f06f4177fb7e51a777b8c1689f743a7bc11ea91d44d2226073a88" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -1479,33 +1776,47 @@ dependencies = [ "libc", "pango-sys", "pkg-config", - "system-deps 6.2.2", + "system-deps", ] [[package]] name = "gdkwayland-sys" -version = "0.15.3" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cca49a59ad8cfdf36ef7330fe7bdfbe1d34323220cc16a0de2679ee773aee2c2" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" dependencies = [ "gdk-sys", "glib-sys", "gobject-sys", "libc", "pkg-config", - "system-deps 6.2.2", + "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.15.1" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b7f8c7a84b407aa9b143877e267e848ff34106578b64d1e0a24bf550716178" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" dependencies = [ "gdk-sys", "glib-sys", "libc", - "system-deps 6.2.2", + "system-deps", "x11", ] @@ -1542,17 +1853,6 @@ dependencies = [ "windows-targets 0.48.5", ] -[[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.15" @@ -1606,49 +1906,54 @@ dependencies = [ [[package]] name = "gio" -version = "0.15.12" +version = "0.18.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" dependencies = [ - "bitflags 1.3.2", "futures-channel", "futures-core", "futures-io", + "futures-util", "gio-sys", "glib", "libc", "once_cell", + "pin-project-lite", + "smallvec", "thiserror 1.0.63", ] [[package]] name = "gio-sys" -version = "0.15.10" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32157a475271e2c4a023382e9cab31c4584ee30a97da41d3c4e9fdd605abcf8d" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.2.2", + "system-deps", "winapi", ] [[package]] name = "glib" -version = "0.15.12" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.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.63", @@ -1656,27 +1961,26 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.15.13" +version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10c6ae9f6fa26f4fb2ac16b528d138d971ead56141de489f8111e259b9df3c4a" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" dependencies = [ - "anyhow", "heck 0.4.1", - "proc-macro-crate", + "proc-macro-crate 2.0.2", "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] name = "glib-sys" -version = "0.15.10" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" dependencies = [ "libc", - "system-deps 6.2.2", + "system-deps", ] [[package]] @@ -1686,37 +1990,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] -name = "globset" -version = "0.4.14" +name = "global-hotkey" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7" dependencies = [ - "aho-corasick", - "bstr", - "log", - "regex-automata", - "regex-syntax", + "crossbeam-channel", + "keyboard-types", + "objc2 0.6.4", + "objc2-app-kit", + "once_cell", + "serde", + "thiserror 2.0.12", + "windows-sys 0.59.0", + "x11rb", + "xkeysym", ] [[package]] name = "gobject-sys" -version = "0.15.10" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" dependencies = [ "glib-sys", "libc", - "system-deps 6.2.2", + "system-deps", ] [[package]] name = "gtk" -version = "0.15.5" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e3004a2d5d6d8b5057d2b57b3712c9529b62e82c77f25c1fecde1fd5c23bd0" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" dependencies = [ "atk", - "bitflags 1.3.2", "cairo-rs", "field-offset", "futures-channel", @@ -1727,16 +2035,15 @@ dependencies = [ "gtk-sys", "gtk3-macros", "libc", - "once_cell", "pango", "pkg-config", ] [[package]] name = "gtk-sys" -version = "0.15.3" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5bc2f0587cba247f60246a0ca11fe25fb733eabc3de12d1965fc07efab87c84" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" dependencies = [ "atk-sys", "cairo-sys-rs", @@ -1747,21 +2054,20 @@ dependencies = [ "gobject-sys", "libc", "pango-sys", - "system-deps 6.2.2", + "system-deps", ] [[package]] name = "gtk3-macros" -version = "0.15.6" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "684c0456c086e8e7e9af73ec5b84e35938df394712054550e81558d21c44ab0d" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" dependencies = [ - "anyhow", - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -1776,7 +2082,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.7.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -1795,7 +2101,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.1.0", - "indexmap 2.7.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -1824,17 +2130,14 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" dependencies = [ - "foldhash", + "foldhash 0.1.5", ] [[package]] -name = "heck" -version = "0.3.3" +name = "hashbrown" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "heck" @@ -1860,6 +2163,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" +[[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" @@ -1877,16 +2186,12 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.26.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" dependencies = [ "log", - "mac", "markup5ever", - "proc-macro2", - "quote", - "syn 1.0.109", ] [[package]] @@ -1897,7 +2202,7 @@ checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" dependencies = [ "bytes", "fnv", - "itoa 1.0.11", + "itoa", ] [[package]] @@ -1908,7 +2213,7 @@ checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", - "itoa 1.0.11", + "itoa", ] [[package]] @@ -1945,12 +2250,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "http-range" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573" - [[package]] name = "httparse" version = "1.9.4" @@ -1978,7 +2277,7 @@ dependencies = [ "http-body 0.4.6", "httparse", "httpdate", - "itoa 1.0.11", + "itoa", "pin-project-lite", "socket2 0.5.10", "tokio", @@ -2000,7 +2299,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "httparse", - "itoa 1.0.11", + "itoa", "pin-project-lite", "smallvec", "tokio", @@ -2024,19 +2323,6 @@ dependencies = [ "tower-service", ] -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper 0.14.30", - "native-tls", - "tokio", - "tokio-native-tls", -] - [[package]] name = "hyper-tls" version = "0.6.0" @@ -2072,7 +2358,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.5.10", - "system-configuration 0.6.1", + "system-configuration", "tokio", "tower-service", "tracing", @@ -2104,12 +2390,12 @@ dependencies = [ [[package]] name = "ico" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.13", ] [[package]] @@ -2263,22 +2549,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "ignore" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" -dependencies = [ - "crossbeam-deque", - "globset", - "log", - "memchr", - "regex-automata", - "same-file", - "walkdir", - "winapi-util", -] - [[package]] name = "image" version = "0.24.9" @@ -2292,7 +2562,7 @@ dependencies = [ "gif", "jpeg-decoder", "num-traits", - "png", + "png 0.17.13", "qoi", "tiff", ] @@ -2306,7 +2576,7 @@ dependencies = [ "bytemuck", "byteorder-lite", "num-traits", - "png", + "png 0.17.13", "tiff", "zune-core", "zune-jpeg", @@ -2325,20 +2595,21 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.17.0", "serde", + "serde_core", ] [[package]] name = "infer" -version = "0.13.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f551f8c3a39f68f986517db0d1759de85881894fdc7db798bd2a9df9cb04b7fc" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" dependencies = [ "cfb", ] @@ -2359,15 +2630,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", -] - [[package]] name = "ipnet" version = "2.9.0" @@ -2384,6 +2646,15 @@ dependencies = [ "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-terminal" version = "0.4.13" @@ -2395,6 +2666,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[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 = "itertools" version = "0.14.0" @@ -2404,12 +2685,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.11" @@ -2418,9 +2693,9 @@ checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "javascriptcore-rs" -version = "0.16.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf053e7843f2812ff03ef5afe34bb9c06ffee120385caad4f6b9967fcd37d41c" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" dependencies = [ "bitflags 1.3.2", "glib", @@ -2429,28 +2704,14 @@ dependencies = [ [[package]] name = "javascriptcore-rs-sys" -version = "0.4.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "905fbb87419c5cde6e3269537e4ea7d46431f3008c5d057e915ef3f115e7793c" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 5.0.0", -] - -[[package]] -name = "jni" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror 1.0.63", - "walkdir", + "system-deps", ] [[package]] @@ -2495,19 +2756,21 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] [[package]] name = "json-patch" -version = "2.0.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1fb8864823fad91877e6caea0baca82e49e8db50f8e5c9f9a453e27d3330fc" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" dependencies = [ "jsonptr", "serde", @@ -2517,15 +2780,25 @@ dependencies = [ [[package]] name = "jsonptr" -version = "0.4.7" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c6e529149475ca0b2820835d3dce8fcc41c6b943ca608d32f35b449255e4627" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" dependencies = [ - "fluent-uri", "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.6.0", + "serde", + "unicode-segmentation", +] + [[package]] name = "keyring" version = "3.6.2" @@ -2540,19 +2813,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "kuchikiki" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" -dependencies = [ - "cssparser", - "html5ever", - "indexmap 1.9.3", - "matches", - "selectors", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -2571,6 +2831,30 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" +[[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 0.7.4", + "once_cell", +] + [[package]] name = "libc" version = "0.2.183" @@ -2586,6 +2870,16 @@ dependencies = [ "pkg-config", ] +[[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 = "libloading" version = "0.8.6" @@ -2697,33 +2991,15 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "mac" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" - -[[package]] -name = "malloc_buf" -version = "0.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" -dependencies = [ - "libc", -] - [[package]] name = "markup5ever" -version = "0.11.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" dependencies = [ "log", - "phf 0.10.1", - "phf_codegen 0.10.0", - "string_cache", - "string_cache_codegen", "tendril", + "web_atoms", ] [[package]] @@ -2735,12 +3011,6 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "matches" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" - [[package]] name = "maybe-owned" version = "0.3.4" @@ -2770,7 +3040,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mindwork-ai-studio" -version = "26.5.1" +version = "26.5.2" dependencies = [ "aes", "arboard", @@ -2787,14 +3057,13 @@ dependencies = [ "keyring", "log", "once_cell", - "openssl", "pbkdf2", "pdfium-render", "pptx-to-md", "rand 0.10.1", "rand_chacha 0.10.0", "rcgen", - "reqwest 0.13.2", + "reqwest", "rocket", "serde", "serde_json", @@ -2802,9 +3071,13 @@ dependencies = [ "strum_macros", "sys-locale", "sysinfo", - "tar", "tauri", "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-global-shortcut", + "tauri-plugin-opener", + "tauri-plugin-shell", + "tauri-plugin-updater", "tauri-plugin-window-state", "tempfile", "time", @@ -2842,6 +3115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -2856,6 +3130,27 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "muda" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae8844f63b5b118e334e205585b8c5c17b984121dbdb179d44aeb087ffad3cb" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.0", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.12", + "windows-sys 0.61.2", +] + [[package]] name = "multer" version = "3.1.0" @@ -2894,28 +3189,24 @@ dependencies = [ [[package]] name = "ndk" -version = "0.6.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "jni-sys", + "log", "ndk-sys", "num_enum", + "raw-window-handle", "thiserror 1.0.63", ] -[[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.3.0" +version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ "jni-sys", ] @@ -2926,12 +3217,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nodrop" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" - [[package]] name = "nom" version = "7.1.3" @@ -3051,53 +3336,50 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.5.11" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.5.11" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] -name = "objc" -version = "0.2.7" +name = "objc-sys" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" -dependencies = [ - "malloc_buf", - "objc_exception", -] +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" [[package]] -name = "objc-foundation" -version = "0.1.1" +name = "objc2" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ - "block", - "objc", - "objc_id", + "objc-sys", + "objc2-encode", ] [[package]] name = "objc2" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", + "objc2-exception-helper", ] [[package]] @@ -3107,9 +3389,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" dependencies = [ "bitflags 2.6.0", - "objc2", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c1948a9be5f469deadbd6bcb86ad7ff9e47b4f632380139722f7d9840c0d42c" +dependencies = [ + "bitflags 2.6.0", + "objc2 0.6.4", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.0", ] [[package]] @@ -3120,7 +3425,7 @@ checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.6.0", "dispatch2", - "objc2", + "objc2 0.6.4", ] [[package]] @@ -3130,17 +3435,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02" dependencies = [ "bitflags 2.6.0", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", ] +[[package]] +name = "objc2-core-image" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ffa6bea72bf42c78b0b34e89c0bafac877d5f80bf91e159a5d96ea7f693ca56" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d31f4c5b5192304996badc466aeadffe1411d73a9bbd3b18b6b2ee9d048b07bd" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.0", +] + [[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.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.6.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", +] + [[package]] name = "objc2-foundation" version = "0.3.0" @@ -3148,7 +3494,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" dependencies = [ "bitflags 2.6.0", - "objc2", + "block2 0.6.2", + "libc", + "objc2 0.6.4", "objc2-core-foundation", ] @@ -3169,26 +3517,101 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19" dependencies = [ "bitflags 2.6.0", - "objc2", + "objc2 0.6.4", "objc2-core-foundation", ] [[package]] -name = "objc_exception" -version = "0.1.2" +name = "objc2-metal" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad970fb455818ad6cba4c122ad012fae53ae8b4795f86378bce65e4f6bab2ca4" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "cc", + "bitflags 2.6.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] -name = "objc_id" -version = "0.1.1" +name = "objc2-osa-kit" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +checksum = "a1ac59da3ceebc4a82179b35dc550431ad9458f9cc326e053f49ba371ce76c5a" dependencies = [ - "objc", + "bitflags 2.6.0", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.6.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fb3794501bb1bee12f08dcad8c61f2a5875791ad1c6f47faa71a0f033f20071" +dependencies = [ + "bitflags 2.6.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "777a571be14a42a3990d4ebedaeb8b54cd17377ec21b92e8200ac03797b3bee1" +dependencies = [ + "bitflags 2.6.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.3.0", + "objc2-quartz-core 0.3.0", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670fe793adbf3b5e93686d48a05a7ed7ee53dfa65d106ced4805fae8969059b2" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.0", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce" +dependencies = [ + "bitflags 2.6.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.0", ] [[package]] @@ -3208,12 +3631,14 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "open" -version = "3.2.0" +version = "5.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2078c0039e6a54a0c42c28faa984e115fb4c2d5bf2208f77d1961002df8576f8" +checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd" dependencies = [ + "dunce", + "is-wsl", + "libc", "pathdiff", - "windows-sys 0.42.0", ] [[package]] @@ -3224,7 +3649,7 @@ checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" dependencies = [ "bitflags 2.6.0", "cfg-if", - "foreign-types", + "foreign-types 0.3.2", "libc", "once_cell", "openssl-macros", @@ -3276,6 +3701,22 @@ dependencies = [ "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 = "os_pipe" version = "1.2.0" @@ -3287,12 +3728,26 @@ dependencies = [ ] [[package]] -name = "pango" -version = "0.15.10" +name = "osakit" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" +checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" dependencies = [ - "bitflags 1.3.2", + "objc2 0.6.4", + "objc2-foundation 0.3.0", + "objc2-osa-kit", + "serde", + "serde_json", + "thiserror 2.0.12", +] + +[[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", @@ -3301,16 +3756,22 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.15.10" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2a00081cde4661982ed91d80ef437c20eacaf6aa1a5962c0279ae194662c3aa" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" dependencies = [ "glib-sys", "gobject-sys", "libc", - "system-deps 6.2.2", + "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.3" @@ -3365,7 +3826,7 @@ dependencies = [ "image 0.25.2", "itertools", "js-sys", - "libloading", + "libloading 0.8.6", "log", "maybe-owned", "once_cell", @@ -3417,106 +3878,43 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "phf" -version = "0.8.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ - "phf_macros 0.8.0", - "phf_shared 0.8.0", - "proc-macro-hack", -] - -[[package]] -name = "phf" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" -dependencies = [ - "phf_shared 0.10.0", -] - -[[package]] -name = "phf" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" -dependencies = [ - "phf_macros 0.11.2", - "phf_shared 0.11.2", + "phf_macros", + "phf_shared", + "serde", ] [[package]] name = "phf_codegen" -version = "0.8.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbffee61585b0411840d3ece935cce9cb6321f01c45477d30066498cd5e1a815" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", -] - -[[package]] -name = "phf_codegen" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" -dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", + "phf_generator", + "phf_shared", ] [[package]] name = "phf_generator" -version = "0.8.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17367f0cc86f2d25802b2c26ee58a7b23faeccf78a396094c13dced0d0182526" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" 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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" -dependencies = [ - "phf_shared 0.11.2", - "rand 0.8.5", + "fastrand", + "phf_shared", ] [[package]] name = "phf_macros" -version = "0.8.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6fde18ff429ffc8fe78e2bf7f8b7a5a5a6e2a8b58bc5a9ac69198bbda9189c" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ - "phf_generator 0.8.0", - "phf_shared 0.8.0", - "proc-macro-hack", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "phf_macros" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", + "phf_generator", + "phf_shared", "proc-macro2", "quote", "syn 2.0.117", @@ -3524,27 +3922,9 @@ dependencies = [ [[package]] name = "phf_shared" -version = "0.8.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" -dependencies = [ - "siphasher", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher", -] - -[[package]] -name = "phf_shared" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" dependencies = [ "siphasher", ] @@ -3555,6 +3935,17 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +[[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 = "piston-float" version = "1.0.1" @@ -3574,7 +3965,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" dependencies = [ "base64 0.22.1", - "indexmap 2.7.0", + "indexmap 2.14.0", "quick-xml 0.32.0", "serde", "time", @@ -3593,6 +3984,33 @@ dependencies = [ "miniz_oxide 0.7.4", ] +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.6.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.5", +] + +[[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 0.5.2", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3645,6 +4063,25 @@ dependencies = [ "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.11+spec-1.1.0", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -3669,17 +4106,11 @@ dependencies = [ "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.92" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] @@ -3796,20 +4227,6 @@ 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" @@ -3842,16 +4259,6 @@ dependencies = [ "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" @@ -3882,15 +4289,6 @@ dependencies = [ "rand_core 0.10.0", ] -[[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" @@ -3916,29 +4314,11 @@ 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.5.2" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "rayon" @@ -3994,13 +4374,13 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.5" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ "getrandom 0.2.15", "libredox", - "thiserror 1.0.63", + "thiserror 2.0.12", ] [[package]] @@ -4052,48 +4432,6 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" -[[package]] -name = "reqwest" -version = "0.11.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" -dependencies = [ - "base64 0.21.7", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.30", - "hyper-tls 0.5.0", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration 0.5.1", - "tokio", - "tokio-native-tls", - "tokio-util", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams", - "web-sys", - "winreg 0.50.0", -] - [[package]] name = "reqwest" version = "0.13.2" @@ -4104,13 +4442,14 @@ dependencies = [ "bytes", "encoding_rs", "futures-core", + "futures-util", "h2 0.4.5", "http 1.1.0", "http-body 1.0.1", "http-body-util", "hyper 1.6.0", "hyper-rustls", - "hyper-tls 0.6.0", + "hyper-tls", "hyper-util", "js-sys", "log", @@ -4122,41 +4461,45 @@ dependencies = [ "rustls 0.23.28", "rustls-pki-types", "rustls-platform-verifier", - "sync_wrapper 1.0.2", + "serde", + "serde_json", + "sync_wrapper", "tokio", "tokio-native-tls", "tokio-rustls 0.26.1", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", ] [[package]] name = "rfd" -version = "0.10.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0149778bd99b6959285b0933288206090c50e2327f47a9c463bfdbf45c8823ea" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" dependencies = [ - "block", - "dispatch", + "block2 0.6.2", + "dispatch2", "glib-sys", "gobject-sys", "gtk-sys", "js-sys", - "lazy_static", "log", - "objc", - "objc-foundation", - "objc_id", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.0", "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.37.0", + "windows-sys 0.60.2", ] [[package]] @@ -4187,7 +4530,7 @@ dependencies = [ "either", "figment", "futures", - "indexmap 2.7.0", + "indexmap 2.14.0", "log", "memchr", "multer", @@ -4200,7 +4543,7 @@ dependencies = [ "rocket_http", "serde", "serde_json", - "state 0.6.0", + "state", "tempfile", "time", "tokio", @@ -4219,7 +4562,7 @@ checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46" dependencies = [ "devise", "glob", - "indexmap 2.7.0", + "indexmap 2.14.0", "proc-macro2", "quote", "rocket_http", @@ -4239,7 +4582,7 @@ dependencies = [ "futures", "http 0.2.12", "hyper 0.14.30", - "indexmap 2.7.0", + "indexmap 2.14.0", "log", "memchr", "pear", @@ -4251,7 +4594,7 @@ dependencies = [ "serde", "smallvec", "stable-pattern", - "state 0.6.0", + "state", "time", "tokio", "tokio-rustls 0.24.1", @@ -4334,6 +4677,7 @@ checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ "aws-lc-rs", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki 0.103.10", "subtle", @@ -4379,7 +4723,7 @@ checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ "core-foundation 0.10.0", "core-foundation-sys", - "jni 0.21.1", + "jni", "log", "once_cell", "rustls 0.23.28", @@ -4426,12 +4770,6 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" -[[package]] -name = "ryu" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" - [[package]] name = "same-file" version = "1.0.6" @@ -4450,6 +4788,33 @@ dependencies = [ "windows-sys 0.52.0", ] +[[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_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 = "scoped-tls" version = "1.0.1" @@ -4510,22 +4875,21 @@ dependencies = [ [[package]] name = "selectors" -version = "0.22.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.6.0", "cssparser", "derive_more", - "fxhash", "log", - "matches", - "phf 0.8.0", - "phf_codegen 0.8.0", + "new_debug_unreachable", + "phf", + "phf_codegen", "precomputed-hash", + "rustc-hash", "servo_arc", "smallvec", - "thin-slice", ] [[package]] @@ -4547,6 +4911,18 @@ dependencies = [ "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" @@ -4567,14 +4943,24 @@ dependencies = [ "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 = [ - "indexmap 2.7.0", - "itoa 1.0.11", + "itoa", "memchr", "serde", "serde_core", @@ -4602,15 +4988,12 @@ dependencies = [ ] [[package]] -name = "serde_urlencoded" -version = "0.7.1" +name = "serde_spanned" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ - "form_urlencoded", - "itoa 1.0.11", - "ryu", - "serde", + "serde_core", ] [[package]] @@ -4623,7 +5006,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.7.0", + "indexmap 2.14.0", "serde", "serde_derive", "serde_json", @@ -4667,11 +5050,10 @@ dependencies = [ [[package]] name = "servo_arc" -version = "0.1.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98238b800e0d1576d8b6e3de32827c2d74bee68bb97748dcf5071fb53965432" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" dependencies = [ - "nodrop", "stable_deref_trait", ] @@ -4739,9 +5121,9 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "siphasher" -version = "0.3.11" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" [[package]] name = "slab" @@ -4779,31 +5161,51 @@ dependencies = [ ] [[package]] -name = "soup2" -version = "0.2.1" +name = "softbuffer" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b4d76501d8ba387cf0fefbe055c3e0a59891d09f0f995ae4e4b16f6b60f3c0" +checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" dependencies = [ - "bitflags 1.3.2", - "gio", - "glib", - "libc", - "once_cell", - "soup2-sys", + "bytemuck", + "cfg_aliases", + "core-graphics 0.24.0", + "foreign-types 0.5.0", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", + "raw-window-handle", + "redox_syscall 0.5.3", + "wasm-bindgen", + "web-sys", + "windows-sys 0.59.0", ] [[package]] -name = "soup2-sys" -version = "0.2.0" +name = "soup3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "009ef427103fcb17f802871647a7fa6c60cbb654b4c4e4c0ac60a31c5f6dc9cf" +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 = [ - "bitflags 1.3.2", "gio-sys", "glib-sys", "gobject-sys", "libc", - "system-deps 5.0.0", + "system-deps", ] [[package]] @@ -4827,15 +5229,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "state" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe866e1e51e8260c9eed836a042a5e7f6726bb2b411dffeaa712e19c388f23b" -dependencies = [ - "loom", -] - [[package]] name = "state" version = "0.6.0" @@ -4847,26 +5240,24 @@ dependencies = [ [[package]] name = "string_cache" -version = "0.8.7" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", - "once_cell", "parking_lot", - "phf_shared 0.10.0", + "phf_shared", "precomputed-hash", - "serde", ] [[package]] name = "string_cache_codegen" -version = "0.5.2" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ - "phf_generator 0.10.0", - "phf_shared 0.10.0", + "phf_generator", + "phf_shared", "proc-macro2", "quote", ] @@ -4895,6 +5286,17 @@ 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" @@ -4917,12 +5319,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -4966,17 +5362,6 @@ dependencies = [ "windows 0.62.2", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "system-configuration-sys 0.5.0", -] - [[package]] name = "system-configuration" version = "0.6.1" @@ -4985,17 +5370,7 @@ checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.6.0", "core-foundation 0.9.4", - "system-configuration-sys 0.6.0", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", + "system-configuration-sys", ] [[package]] @@ -5008,76 +5383,56 @@ dependencies = [ "libc", ] -[[package]] -name = "system-deps" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18db855554db7bd0e73e06cf7ba3df39f97812cb11d3f75e71c39bf45171797e" -dependencies = [ - "cfg-expr 0.9.1", - "heck 0.3.3", - "pkg-config", - "toml 0.5.11", - "version-compare 0.0.11", -] - [[package]] name = "system-deps" version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ - "cfg-expr 0.15.8", + "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.16", - "version-compare 0.2.0", + "toml 0.8.2", + "version-compare", ] [[package]] name = "tao" -version = "0.16.9" +version = "0.35.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575c856fc21e551074869dcfaad8f706412bd5b803dfa0fbf6881c4ff4bfafab" +checksum = "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4" dependencies = [ - "bitflags 1.3.2", - "cairo-rs", - "cc", - "cocoa", - "core-foundation 0.9.4", - "core-graphics", + "bitflags 2.6.0", + "block2 0.6.2", + "core-foundation 0.10.0", + "core-graphics 0.25.0", "crossbeam-channel", - "dispatch", - "gdk", - "gdk-pixbuf", - "gdk-sys", + "dbus", + "dispatch2", + "dlopen2", + "dpi", "gdkwayland-sys", "gdkx11-sys", - "gio", - "glib", - "glib-sys", "gtk", - "image 0.24.9", - "instant", - "jni 0.20.0", - "lazy_static", + "jni", "libc", "log", "ndk", - "ndk-context", "ndk-sys", - "objc", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-foundation 0.3.0", + "objc2-ui-kit", "once_cell", "parking_lot", - "png", + "percent-encoding", "raw-window-handle", - "scopeguard", - "serde", "tao-macros", "unicode-segmentation", - "uuid", - "windows 0.39.0", - "windows-implement 0.39.0", + "url", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", "x11-dl", ] @@ -5111,77 +5466,68 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "1.8.3" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae1f57c291a6ab8e1d2e6b8ad0a35ff769c9925deb8a89de85425ff08762d0c" +checksum = "b93bd86d231f0a8138f11a02a584769fe4b703dc36ae133d783228dbc4801405" dependencies = [ "anyhow", - "base64 0.22.1", "bytes", - "cocoa", - "dirs-next", + "cookie", + "dirs", "dunce", "embed_plist", - "encoding_rs", - "flate2", - "futures-util", - "getrandom 0.2.15", - "glib", + "getrandom 0.3.1", "glob", "gtk", "heck 0.5.0", - "http 0.2.12", - "ignore", - "indexmap 1.9.3", - "infer", + "http 1.1.0", + "jni", + "libc", "log", - "minisign-verify", - "objc", - "once_cell", - "open", - "os_pipe", + "mime", + "muda", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-foundation 0.3.0", + "objc2-ui-kit", + "objc2-web-kit", "percent-encoding", "plist", - "rand 0.8.5", "raw-window-handle", - "regex", - "reqwest 0.11.27", - "rfd", - "semver", + "reqwest", "serde", "serde_json", "serde_repr", "serialize-to-javascript", - "shared_child", - "state 0.5.3", - "tar", + "swift-rs", + "tauri-build", "tauri-macros", "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "tempfile", - "thiserror 1.0.63", - "time", + "thiserror 2.0.12", "tokio", + "tray-icon", "url", - "uuid", "webkit2gtk", "webview2-com", - "windows 0.39.0", - "zip 0.6.6", + "window-vibrancy", + "windows 0.61.3", ] [[package]] name = "tauri-build" -version = "1.5.6" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2db08694eec06f53625cfc6fff3a363e084e5e9a238166d2989996413c346453" +checksum = "3a318b234cc2dea65f575467bafcfb76286bce228ebc3778e337d61d03213007" dependencies = [ "anyhow", "cargo_toml", - "dirs-next", + "dirs", + "glob", "heck 0.5.0", "json-patch", + "schemars", "semver", "serde", "serde_json", @@ -5192,137 +5538,307 @@ dependencies = [ [[package]] name = "tauri-codegen" -version = "1.4.6" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53438d78c4a037ffe5eafa19e447eea599bedfb10844cb08ec53c2471ac3ac3f" +checksum = "6bd11644962add2549a60b7e7c6800f17d7020156e02f516021d8103e80cc528" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "brotli", "ico", "json-patch", "plist", - "png", + "png 0.17.13", "proc-macro2", "quote", - "regex", "semver", "serde", "serde_json", "sha2", + "syn 2.0.117", "tauri-utils", - "thiserror 1.0.63", + "thiserror 2.0.12", "time", + "url", "uuid", "walkdir", ] [[package]] name = "tauri-macros" -version = "1.4.7" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233988ac08c1ed3fe794cd65528d48d8f7ed4ab3895ca64cdaa6ad4d00c45c0b" +checksum = "fed9d3742a37a355d2e47c9af924e9fbc112abb76f9835d35d4780e318419502" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.117", "tauri-codegen", "tauri-utils", ] [[package]] -name = "tauri-plugin-window-state" -version = "0.1.1" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=v1#1a38991689b60aafdf502072082c108ad9149a61" +name = "tauri-plugin" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eefb2c18e8a605c23edb48fc56bb77381199e1a1e7f6ff0c9b970afe7b3cb8ee" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.12", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation 0.3.0", + "percent-encoding", + "schemars", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.12", + "toml 1.1.2+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-global-shortcut" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "424af23c7e88d05e4a1a6fc2c7be077912f8c76bd7900fd50aa2b7cbf5a2c405" +dependencies = [ + "global-hotkey", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.12", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation 0.3.0", + "open", + "schemars", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.12", + "url", + "windows 0.61.3", + "zbus", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.12", + "tokio", +] + +[[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 1.1.0", + "infer", + "log", + "minisign-verify", + "osakit", + "percent-encoding", + "reqwest", + "rustls 0.23.28", + "semver", + "serde", + "serde_json", + "tar", + "tauri", + "tauri-plugin", + "tempfile", + "thiserror 2.0.12", + "time", + "tokio", + "url", + "windows-sys 0.60.2", + "zip 4.6.1", +] + +[[package]] +name = "tauri-plugin-window-state" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" dependencies = [ - "bincode", "bitflags 2.6.0", "log", "serde", "serde_json", "tauri", - "thiserror 1.0.63", + "tauri-plugin", + "thiserror 2.0.12", ] [[package]] name = "tauri-runtime" -version = "0.14.6" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8066855882f00172935e3fa7d945126580c34dcbabab43f5d4f0c2398a67d47b" +checksum = "8fef478ba1d2ac21c2d528740b24d0cb315e1e8b1111aae53fafac34804371fc" dependencies = [ + "cookie", + "dpi", "gtk", - "http 0.2.12", - "http-range", - "rand 0.8.5", + "http 1.1.0", + "jni", + "objc2 0.6.4", + "objc2-ui-kit", + "objc2-web-kit", "raw-window-handle", "serde", "serde_json", "tauri-utils", - "thiserror 1.0.63", + "thiserror 2.0.12", "url", - "uuid", + "webkit2gtk", "webview2-com", - "windows 0.39.0", + "windows 0.61.3", ] [[package]] name = "tauri-runtime-wry" -version = "0.14.11" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce361fec1e186705371f1c64ae9dd2a3a6768bc530d0a2d5e75a634bb416ad4d" +checksum = "a3989df2ae1c476404fe0a2e8ffc4cfbde97e51efd613c2bb5355fbc9ab52cf0" dependencies = [ - "cocoa", "gtk", + "http 1.1.0", + "jni", + "log", + "objc2 0.6.4", + "objc2-app-kit", + "once_cell", "percent-encoding", - "rand 0.8.5", "raw-window-handle", + "softbuffer", + "tao", "tauri-runtime", "tauri-utils", - "uuid", + "url", "webkit2gtk", "webview2-com", - "windows 0.39.0", + "windows 0.61.3", "wry", ] [[package]] name = "tauri-utils" -version = "1.6.2" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c357952645e679de02cd35007190fcbce869b93ffc61b029f33fe02648453774" +checksum = "d57200389a2f82b4b0a40ae29ca19b6978116e8f4d4e974c3234ce40c0ffbdec" dependencies = [ + "anyhow", "brotli", + "cargo_metadata", "ctor", + "dom_query", "dunce", "glob", - "heck 0.5.0", - "html5ever", + "http 1.1.0", "infer", "json-patch", - "kuchikiki", "log", "memchr", - "phf 0.11.2", + "phf", + "plist", "proc-macro2", "quote", + "regex", + "schemars", "semver", "serde", + "serde-untagged", "serde_json", "serde_with", - "thiserror 1.0.63", + "swift-rs", + "thiserror 2.0.12", + "toml 1.1.2+spec-1.1.0", "url", + "urlpattern", + "uuid", "walkdir", - "windows-version", ] [[package]] name = "tauri-winres" -version = "0.1.1" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5993dc129e544393574288923d1ec447c857f3f644187f4fbf7d9a875fbfc4fb" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" dependencies = [ + "dunce", "embed-resource", - "toml 0.7.8", + "toml 1.1.2+spec-1.1.0", ] [[package]] @@ -5340,21 +5856,14 @@ dependencies = [ [[package]] name = "tendril" -version = "0.4.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" dependencies = [ - "futf", - "mac", + "new_debug_unreachable", "utf-8", ] -[[package]] -name = "thin-slice" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" - [[package]] name = "thiserror" version = "1.0.63" @@ -5423,7 +5932,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", - "itoa 1.0.11", + "itoa", "num-conv", "powerfmt", "serde_core", @@ -5555,72 +6064,124 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.11" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", + "serde_spanned 0.6.7", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", ] [[package]] name = "toml" -version = "0.7.8" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.19.15", + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", ] [[package]] name = "toml" -version = "0.8.16" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81967dd0dd2c1ab0bc3468bd7caecc32b8a4aa47d0c8c695d8c2b2108168d62c" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ - "serde", - "serde_spanned", - "toml_datetime", - "toml_edit 0.22.22", + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.2", ] [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +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.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +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.7.0", - "serde", - "serde_spanned", - "toml_datetime", + "indexmap 2.14.0", + "toml_datetime 0.6.3", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.7.0", + "indexmap 2.14.0", "serde", - "serde_spanned", - "toml_datetime", - "winnow 0.6.21", + "serde_spanned 0.6.7", + "toml_datetime 0.6.3", + "winnow 0.5.40", ] +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.2", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + [[package]] name = "tower" version = "0.5.2" @@ -5630,7 +6191,7 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tower-layer", "tower-service", @@ -5727,6 +6288,28 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tray-icon" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15edbb0d80583e85ee8df283410038e17314df5cba30da2087a54a85216c0773" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.0", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.12", + "windows-sys 0.61.2", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -5739,6 +6322,12 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3015e6ce46d5ad8751e4a772543a30c7511468070e98e64e20165f8f81155b64" +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.17.0" @@ -5754,6 +6343,17 @@ dependencies = [ "serde", ] +[[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 = "uncased" version = "0.9.10" @@ -5764,6 +6364,47 @@ dependencies = [ "version_check", ] +[[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.12" @@ -5801,6 +6442,18 @@ dependencies = [ "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" @@ -5835,6 +6488,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom 0.2.15", + "serde", ] [[package]] @@ -5858,12 +6512,6 @@ dependencies = [ "piston-float", ] -[[package]] -name = "version-compare" -version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" - [[package]] name = "version-compare" version = "0.2.0" @@ -5915,12 +6563,6 @@ 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.0+wasi-snapshot-preview1" @@ -5956,47 +6598,32 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ - "cfg-if", "js-sys", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -6004,22 +6631,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ + "bumpalo", "proc-macro2", "quote", "syn 2.0.117", - "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] @@ -6041,16 +6668,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.7.0", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] [[package]] name = "wasm-streams" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", @@ -6067,15 +6694,15 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags 2.6.0", "hashbrown 0.15.2", - "indexmap 2.7.0", + "indexmap 2.14.0", "semver", ] [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", @@ -6092,10 +6719,22 @@ dependencies = [ ] [[package]] -name = "webkit2gtk" -version = "0.18.2" +name = "web_atoms" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8f859735e4a452aeb28c6c56a852967a8a76c8eb1cc32dbf931ad28a13d6370" +checksum = "d7cff6eef815df1834fd250e3a2ff436044d82a9f1bc1980ca1dbdf07effc538" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[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", @@ -6111,20 +6750,18 @@ dependencies = [ "javascriptcore-rs", "libc", "once_cell", - "soup2", + "soup3", "webkit2gtk-sys", ] [[package]] name = "webkit2gtk-sys" -version = "0.18.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d76ca6ecc47aeba01ec61e480139dda143796abcae6f83bcddf50d6b5b1dcf3" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" dependencies = [ - "atk-sys", "bitflags 1.3.2", "cairo-sys-rs", - "gdk-pixbuf-sys", "gdk-sys", "gio-sys", "glib-sys", @@ -6132,10 +6769,9 @@ dependencies = [ "gtk-sys", "javascriptcore-rs-sys", "libc", - "pango-sys", "pkg-config", - "soup2-sys", - "system-deps 6.2.2", + "soup3-sys", + "system-deps", ] [[package]] @@ -6149,40 +6785,38 @@ dependencies = [ [[package]] name = "webview2-com" -version = "0.19.1" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a769c9f1a64a8734bde70caafac2b96cada12cd4aefa49196b3a386b8b4178" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows 0.39.0", - "windows-implement 0.39.0", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", ] [[package]] name = "webview2-com-macros" -version = "0.6.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaebe196c01691db62e9e4ca52c5ef1e4fd837dcae27dae3ada599b5a8fd05ac" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] name = "webview2-com-sys" -version = "0.19.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aac48ef20ddf657755fdcda8dfed2a7b4fc7e4581acce6fe9b88c3d64f29dee7" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ - "regex", - "serde", - "serde_json", - "thiserror 1.0.63", - "windows 0.39.0", - "windows-bindgen", - "windows-metadata", + "thiserror 2.0.12", + "windows 0.61.3", + "windows-core 0.61.2", ] [[package]] @@ -6223,30 +6857,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows" -version = "0.37.0" +name = "window-vibrancy" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "windows_aarch64_msvc 0.37.0", - "windows_i686_gnu 0.37.0", - "windows_i686_msvc 0.37.0", - "windows_x86_64_gnu 0.37.0", - "windows_x86_64_msvc 0.37.0", -] - -[[package]] -name = "windows" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" -dependencies = [ - "windows-implement 0.39.0", - "windows_aarch64_msvc 0.39.0", - "windows_i686_gnu 0.39.0", - "windows_i686_msvc 0.39.0", - "windows_x86_64_gnu 0.39.0", - "windows_x86_64_msvc 0.39.0", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.0", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", ] [[package]] @@ -6258,26 +6880,38 @@ dependencies = [ "windows-targets 0.48.5", ] +[[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", + "windows-collections 0.3.2", "windows-core 0.62.2", - "windows-future", - "windows-numerics", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] -name = "windows-bindgen" -version = "0.39.0" +name = "windows-collections" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68003dbd0e38abc0fb85b939240f4bce37c43a5981d3df37ccbaaa981b47cb41" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-metadata", - "windows-tokens", + "windows-core 0.61.2", ] [[package]] @@ -6298,19 +6932,43 @@ 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 0.60.2", + "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" @@ -6319,17 +6977,7 @@ checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ "windows-core 0.62.2", "windows-link 0.2.1", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba01f98f509cb5dc05f4e5fc95e535f78260f15fea8fe1a8abdd08f774f1cee7" -dependencies = [ - "syn 1.0.109", - "windows-tokens", + "windows-threading 0.2.1", ] [[package]] @@ -6367,10 +7015,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-metadata" -version = "0.39.0" +name = "windows-numerics" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ee5e275231f07c6e240d14f34e1b635bf1faa1c76c57cfd59a5cdb9848e4278" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] [[package]] name = "windows-numerics" @@ -6440,21 +7092,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -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-sys" version = "0.45.0" @@ -6464,15 +7101,6 @@ dependencies = [ "windows-targets 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -6572,6 +7200,15 @@ dependencies = [ "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" @@ -6581,12 +7218,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-tokens" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f838de2fe15fe6bac988e74b798f26499a8b21a9d97edec321e79b28d1d7f597" - [[package]] name = "windows-version" version = "0.1.1" @@ -6620,18 +7251,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" - [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -6656,18 +7275,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1" - -[[package]] -name = "windows_i686_gnu" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" - [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -6704,18 +7311,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c" - -[[package]] -name = "windows_i686_msvc" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" - [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -6740,18 +7335,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" - [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -6800,18 +7383,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" - [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -6847,31 +7418,27 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.21" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6f5bb5257f2407a5425c6e749bfd9692192a73e70a6060516ac04f889087d68" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.50.0" +version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" dependencies = [ "cfg-if", - "windows-sys 0.48.0", -] - -[[package]] -name = "winreg" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", + "windows-sys 0.59.0", ] [[package]] @@ -6911,7 +7478,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck 0.5.0", - "indexmap 2.7.0", + "indexmap 2.14.0", "prettyplease", "syn 2.0.117", "wasm-metadata", @@ -6942,7 +7509,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags 2.6.0", - "indexmap 2.7.0", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -6961,7 +7528,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.7.0", + "indexmap 2.14.0", "log", "semver", "serde", @@ -6985,40 +7552,46 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" [[package]] name = "wry" -version = "0.24.10" +version = "0.55.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00711278ed357350d44c749c286786ecac644e044e4da410d466212152383b45" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" dependencies = [ - "base64 0.13.1", - "block", - "cocoa", - "core-graphics", + "base64 0.22.1", + "block2 0.6.2", + "cookie", "crossbeam-channel", + "dirs", + "dom_query", + "dpi", "dunce", - "gdk", - "gio", - "glib", + "gdkx11", "gtk", - "html5ever", - "http 0.2.12", - "kuchikiki", + "http 1.1.0", + "javascriptcore-rs", + "jni", "libc", - "log", - "objc", - "objc_id", + "ndk", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation 0.3.0", + "objc2-ui-kit", + "objc2-web-kit", "once_cell", - "serde", - "serde_json", + "percent-encoding", + "raw-window-handle", "sha2", - "soup2", - "tao", - "thiserror 1.0.63", + "soup3", + "tao-macros", + "thiserror 2.0.12", "url", "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows 0.39.0", - "windows-implement 0.39.0", + "windows 0.61.3", + "windows-core 0.61.2", + "windows-version", + "x11-dl", ] [[package]] @@ -7088,6 +7661,12 @@ dependencies = [ "rustix 0.38.34", ] +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + [[package]] name = "xz2" version = "0.1.7" @@ -7139,6 +7718,67 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3bcbf15c8708d7fc1be0c993622e0a5cbd5e8b52bfa40afa4c3e0cd8d724ac1" +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 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.2", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff" +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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.2", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.17" @@ -7222,17 +7862,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "zip" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" -dependencies = [ - "byteorder", - "crc32fast", - "crossbeam-utils", -] - [[package]] name = "zip" version = "2.5.0" @@ -7249,7 +7878,7 @@ dependencies = [ "flate2", "getrandom 0.3.1", "hmac", - "indexmap 2.7.0", + "indexmap 2.14.0", "lzma-rs", "memchr", "pbkdf2", @@ -7261,6 +7890,18 @@ dependencies = [ "zstd", ] +[[package]] +name = "zip" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1" +dependencies = [ + "arbitrary", + "crc32fast", + "indexmap 2.14.0", + "memchr", +] + [[package]] name = "zip" version = "7.4.0" @@ -7269,7 +7910,7 @@ checksum = "cc12baa6db2b15a140161ce53d72209dacea594230798c24774139b54ecaa980" dependencies = [ "crc32fast", "flate2", - "indexmap 2.7.0", + "indexmap 2.14.0", "memchr", "typed-path", "zopfli", @@ -7352,3 +7993,43 @@ checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" dependencies = [ "zune-core", ] + +[[package]] +name = "zvariant" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c1567a6ec68df868cbbfde844cfc6d81649fe5109a62b116b19fabd53e618ee" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.2", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d464f5733ffa07a3164d656f18533caace9d0638596721355d73256a410d691" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 1.0.2", +] diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 0d7739ab..97328e92 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -1,16 +1,19 @@ [package] name = "mindwork-ai-studio" -version = "26.5.1" -edition = "2021" +version = "26.5.2" +edition = "2024" description = "MindWork AI Studio" authors = ["Thorsten Sommer"] [build-dependencies] -tauri-build = { version = "1.5.6", features = [] } +tauri-build = { version = "2.6.1", features = [] } [dependencies] -tauri = { version = "1.8.3", features = [ "http-all", "updater", "shell-sidecar", "shell-open", "dialog", "global-shortcut"] } -tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" } +tauri = { version = "2.11.1", features = [] } +tauri-plugin-window-state = { version = "2.4.1" } +tauri-plugin-shell = "2.3.5" +tauri-plugin-dialog = "2.7.1" +tauri-plugin-opener = "2.5.4" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" keyring = { version = "3.6.2", features = ["apple-native", "windows-native", "sync-secret-service"] } @@ -32,7 +35,7 @@ pbkdf2 = "0.12.2" hmac = "0.12.1" sha2 = "0.10.8" rcgen = { version = "0.14.7", features = ["pem"] } -file-format = "0.28.0" +file-format = "0.29.0" calamine = "0.34.0" pdfium-render = "0.8.37" sys-locale = "0.3.2" @@ -45,17 +48,17 @@ sysinfo = "0.38.4" # Fixes security vulnerability downstream, where the upstream is not fixed yet: time = "0.3.47" # -> Rocket bytes = "1.11.1" # -> almost every dependency -tar = "0.4.45" # -> Tauri v1 [target.'cfg(target_os = "linux")'.dependencies] # See issue https://github.com/tauri-apps/tauri/issues/4470 reqwest = { version = "0.13.2", features = ["native-tls-vendored"] } -# Fixes security vulnerability downstream, where the upstream is not fixed yet: -openssl = "0.10.76" # -> reqwest, Tauri v1 - [target.'cfg(target_os = "windows")'.dependencies] windows-registry = "0.6.1" +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +tauri-plugin-global-shortcut = "2" +tauri-plugin-updater = "2.10.0" + [features] custom-protocol = ["tauri/custom-protocol"] diff --git a/runtime/build.rs b/runtime/build.rs index c4d1f749..80a92985 100644 --- a/runtime/build.rs +++ b/runtime/build.rs @@ -53,6 +53,18 @@ fn update_cargo_toml(cargo_path: &str, version: &str) { let cargo_toml_lines = cargo_toml.lines(); let mut new_cargo_toml = String::new(); + // Return early when the version already matches to avoid unnecessary rewrites. + let current_version = cargo_toml.lines().find_map(|line| { + let trimmed = line.trim_start(); + let rest = trimmed.strip_prefix("\"version\": ")?; + let quoted = rest.strip_prefix('"')?; + let end_idx = quoted.find('"')?; + Some("ed[..end_idx]) + }); + if current_version == Some(version) { + return; + } + for line in cargo_toml_lines { if line.starts_with("version = ") { new_cargo_toml.push_str(&format!("version = \"{version}\"")); @@ -67,6 +79,19 @@ fn update_cargo_toml(cargo_path: &str, version: &str) { fn update_tauri_conf(tauri_conf_path: &str, version: &str) { let tauri_conf = std::fs::read_to_string(tauri_conf_path).unwrap(); + + // Return early when the version already matches to avoid unnecessary rewrites. + let current_version = tauri_conf.lines().find_map(|line| { + let trimmed = line.trim_start(); + let rest = trimmed.strip_prefix("\"version\": ")?; + let quoted = rest.strip_prefix('"')?; + let end_idx = quoted.find('"')?; + Some("ed[..end_idx]) + }); + if current_version == Some(version) { + return; + } + let tauri_conf_lines = tauri_conf.lines(); let mut new_tauri_conf = String::new(); @@ -75,7 +100,7 @@ fn update_tauri_conf(tauri_conf_path: &str, version: &str) { // "version": "0.1.0-alpha.0" // Please notice, that the version number line might have a leading tab, etc. if line.contains("\"version\": ") { - new_tauri_conf.push_str(&format!("\t\"version\": \"{version}\"")); + new_tauri_conf.push_str(&format!(" \"version\": \"{version}\",")); } else { new_tauri_conf.push_str(line); } diff --git a/runtime/capabilities/default.json b/runtime/capabilities/default.json new file mode 100644 index 00000000..86f14897 --- /dev/null +++ b/runtime/capabilities/default.json @@ -0,0 +1,34 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Default capability for MindWork AI Studio", + "remote": { + "urls": [ + "http://localhost:*" + ] + }, + "windows": [ + "main" + ], + "permissions": [ + "core:default", + "updater:default", + "opener:default", + "shell:allow-open", + { + "identifier": "shell:allow-spawn", + "allow": [ + { + "name": "mindworkAIStudioServer", + "sidecar": true, + "args": true + }, + { + "name": "qdrant", + "sidecar": true, + "args": true + } + ] + } + ] +} diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index 70233631..d33fdc52 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -9,9 +9,12 @@ use rocket::serde::json::Json; use rocket::serde::Serialize; use serde::Deserialize; use strum_macros::Display; -use tauri::updater::UpdateResponse; -use tauri::{FileDropEvent, GlobalShortcutManager, UpdaterEvent, RunEvent, Manager, PathResolver, Window, WindowEvent, generate_context}; -use tauri::api::dialog::blocking::FileDialogBuilder; +use tauri::{DragDropEvent,RunEvent, Manager, WindowEvent, generate_context}; +use tauri::path::PathResolver; +use tauri::WebviewWindow; +use tauri_plugin_updater::{UpdaterExt, Update}; +use tauri_plugin_global_shortcut::GlobalShortcutExt; +use tauri_plugin_opener::OpenerExt; use tokio::sync::broadcast; use tokio::time; use crate::api_token::APIToken; @@ -24,10 +27,10 @@ use crate::qdrant::{cleanup_qdrant, start_qdrant_server, stop_qdrant_server}; use crate::dotnet::create_startup_env_file; /// The Tauri main window. -static MAIN_WINDOW: Lazy>> = Lazy::new(|| Mutex::new(None)); +pub static MAIN_WINDOW: Lazy>> = Lazy::new(|| Mutex::new(None)); /// The update response coming from the Tauri updater. -static CHECK_UPDATE_RESPONSE: Lazy>>> = Lazy::new(|| Mutex::new(None)); +static CHECK_UPDATE_RESPONSE: Lazy>> = Lazy::new(|| Mutex::new(None)); /// The event broadcast sender for Tauri events. static EVENT_BROADCAST: Lazy>>> = Lazy::new(|| Mutex::new(None)); @@ -35,6 +38,9 @@ static EVENT_BROADCAST: Lazy>>> = Lazy::ne /// Stores the currently registered global shortcuts (name -> shortcut string). static REGISTERED_SHORTCUTS: Lazy>> = Lazy::new(|| Mutex::new(HashMap::new())); +/// Stores the localhost origin of the Blazor app after the .NET server is ready. +static APPROVED_APP_URL: Lazy>> = Lazy::new(|| Mutex::new(None)); + /// Enum identifying global keyboard shortcuts. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Display)] #[strum(serialize_all = "SCREAMING_SNAKE_CASE")] @@ -76,10 +82,34 @@ pub fn start_tauri() { }); let app = tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_opener::init()) + .plugin( + tauri::plugin::Builder::::new("external-link-handler") + .on_navigation(|webview, url| { + if !should_open_in_system_browser(webview, url) { + return true; + } + + match webview.app_handle().opener().open_url(url.as_str(), None::<&str>) { + Ok(_) => { + info!(Source = "Tauri"; "Opening external URL in system browser: {url}"); + }, + Err(error) => { + error!(Source = "Tauri"; "Failed to open external URL '{url}' in system browser: {error}"); + }, + } + false + }) + .build(), + ) + .plugin(tauri_plugin_global_shortcut::Builder::new().build()) + .plugin(tauri_plugin_updater::Builder::new().build()) .setup(move |app| { // Get the main window: - let window = app.get_window("main").expect("Failed to get main window."); + let window = app.get_webview_window("main").expect("Failed to get main window."); // Register a callback for window events, such as file drops. We have to use // this handler in addition to the app event handler, because file drop events @@ -100,27 +130,27 @@ pub fn start_tauri() { *MAIN_WINDOW.lock().unwrap() = Some(window); info!(Source = "Bootloader Tauri"; "Setup is running."); - let data_path = app.path_resolver().app_local_data_dir().unwrap(); + let data_path = app.path().app_local_data_dir().unwrap(); let data_path = data_path.join("data"); // Get and store the data and config directories: DATA_DIRECTORY.set(data_path.to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the data directory.")).unwrap(); - CONFIG_DIRECTORY.set(app.path_resolver().app_config_dir().unwrap().to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the config directory.")).unwrap(); + CONFIG_DIRECTORY.set(app.path().app_config_dir().unwrap().to_str().unwrap().to_string()).map_err(|_| error!("Was not able to set the config directory.")).unwrap(); if is_dev() { #[cfg(debug_assertions)] create_startup_env_file(); } else { cleanup_dotnet_server(); - start_dotnet_server(); + start_dotnet_server(app.handle().clone()); } cleanup_qdrant(); - start_qdrant_server(app.path_resolver()); + start_qdrant_server(app.handle().clone()); info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {data_path:?}"); switch_to_file_logging(data_path).map_err(|e| error!("Failed to switch logging to file: {e}")).unwrap(); - set_pdfium_path(app.path_resolver()); + set_pdfium_path(app.path()); Ok(()) }) @@ -129,7 +159,7 @@ pub fn start_tauri() { .expect("Error while running Tauri application"); // The app event handler: - app.run(|app_handle, event| { + app.run(|_app_handle, event| { if !matches!(event, RunEvent::MainEventsCleared) { debug!(Source = "Tauri"; "Tauri event received: location=app event handler , event={event:?}"); } @@ -149,54 +179,6 @@ pub fn start_tauri() { } } - RunEvent::Updater(updater_event) => { - match updater_event { - UpdaterEvent::UpdateAvailable { body, date, version } => { - let body_len = body.len(); - info!(Source = "Tauri"; "Updater: update available: body size={body_len} time={date:?} version={version}"); - } - - UpdaterEvent::Pending => { - info!(Source = "Tauri"; "Updater: update is pending!"); - } - - UpdaterEvent::DownloadProgress { chunk_length, content_length: _ } => { - trace!(Source = "Tauri"; "Updater: downloading chunk of {chunk_length} bytes"); - } - - UpdaterEvent::Downloaded => { - info!(Source = "Tauri"; "Updater: update has been downloaded!"); - warn!(Source = "Tauri"; "Try to stop the .NET server now..."); - - if is_prod() { - stop_dotnet_server(); - stop_qdrant_server(); - } else { - warn!(Source = "Tauri"; "Development environment detected; do not stop the .NET server."); - } - } - - UpdaterEvent::Updated => { - info!(Source = "Tauri"; "Updater: app has been updated"); - warn!(Source = "Tauri"; "Try to restart the app now..."); - - if is_prod() { - app_handle.restart(); - } else { - warn!(Source = "Tauri"; "Development environment detected; do not restart the app."); - } - } - - UpdaterEvent::AlreadyUpToDate => { - info!(Source = "Tauri"; "Updater: app is already up to date"); - } - - UpdaterEvent::Error(error) => { - warn!(Source = "Tauri"; "Updater: failed to update: {error}"); - } - } - } - RunEvent::ExitRequested { .. } => { warn!(Source = "Tauri"; "Run event: exit was requested."); stop_qdrant_server(); @@ -217,6 +199,46 @@ pub fn start_tauri() { warn!(Source = "Tauri"; "Tauri app was stopped."); } +fn is_local_host(host: Option<&str>) -> bool { + matches!(host, Some("localhost") | Some("127.0.0.1") | Some("::1") | Some("[::1]")) +} + +fn is_local_http_url(url: &tauri::Url) -> bool { + matches!(url.scheme(), "http" | "https") && is_local_host(url.host_str()) +} + +fn same_origin(left: &tauri::Url, right: &tauri::Url) -> bool { + left.scheme() == right.scheme() + && left.host_str() == right.host_str() + && left.port_or_known_default() == right.port_or_known_default() +} + +fn should_open_in_system_browser(webview: &tauri::Webview, url: &tauri::Url) -> bool { + match url.scheme() { + "mailto" | "tel" => return true, + "http" | "https" => {}, + _ => return false, + } + + if let Some(approved_app_url) = APPROVED_APP_URL.lock().unwrap().as_ref() { + if same_origin(approved_app_url, url) { + return false; + } + + if is_local_http_url(url) { + return true; + } + } + + if let Ok(current_url) = webview.url() { + if same_origin(¤t_url, url) { + return false; + } + } + + !is_local_host(url.host_str()) +} + /// Our event API endpoint for Tauri events. We try to send an endless stream of events to the client. /// If no events are available for a certain time, we send a ping event to keep the connection alive. /// When the client disconnects, the stream is closed. But we try to not lose events in between. @@ -303,23 +325,21 @@ impl Event { /// Creates an Event instance from a Tauri WindowEvent. pub fn from_window_event(window_event: &WindowEvent) -> Self { match window_event { - WindowEvent::FileDrop(drop_event) => { + WindowEvent::DragDrop(drop_event) => { match drop_event { - FileDropEvent::Hovered(files) => Event::new(TauriEventType::FileDropHovered, - files.iter().map(|f| f.to_string_lossy().to_string()).collect(), + DragDropEvent::Enter { paths, .. } => Event::new( + TauriEventType::FileDropHovered, + paths.iter().map(|p| p.display().to_string()).collect(), ), - FileDropEvent::Dropped(files) => Event::new(TauriEventType::FileDropDropped, - files.iter().map(|f| f.to_string_lossy().to_string()).collect(), + DragDropEvent::Drop { paths, .. } => Event::new( + TauriEventType::FileDropDropped, + paths.iter().map(|p| p.display().to_string()).collect(), ), - FileDropEvent::Cancelled => Event::new(TauriEventType::FileDropCanceled, - Vec::new(), - ), + DragDropEvent::Leave => Event::new(TauriEventType::FileDropCanceled, Vec::new()), - _ => Event::new(TauriEventType::Unknown, - Vec::new(), - ), + _ => Event::new(TauriEventType::Unknown, Vec::new()), } }, @@ -380,6 +400,12 @@ pub async fn change_location_to(url: &str) { } } + if let Ok(parsed_url) = tauri::Url::parse(url) { + if is_local_http_url(&parsed_url) { + *APPROVED_APP_URL.lock().unwrap() = Some(parsed_url); + } + } + let js_location_change = format!("window.location = '{url}';"); let main_window = main_window_spawn_clone.lock().unwrap(); let location_change_result = main_window.as_ref().unwrap().eval(js_location_change.as_str()); @@ -402,46 +428,67 @@ pub async fn check_for_update(_token: APIToken) -> Json { }); } - let app_handle = MAIN_WINDOW.lock().unwrap().as_ref().unwrap().app_handle(); - let response = app_handle.updater().check().await; - match response { - Ok(update_response) => match update_response.is_update_available() { - true => { - *CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update_response.clone()); - let new_version = update_response.latest_version(); - info!(Source = "Updater"; "An update to version '{new_version}' is available."); - let changelog = update_response.body(); - Json(CheckUpdateResponse { - update_is_available: true, - error: false, - new_version: new_version.to_string(), - changelog: match changelog { - Some(c) => c.to_string(), - None => String::from(""), - }, - }) - }, - - false => { - info!(Source = "Updater"; "No updates are available."); - Json(CheckUpdateResponse { + let app_handle = { + let main_window = MAIN_WINDOW.lock().unwrap(); + match main_window.as_ref() { + Some(window) => window.app_handle().clone(), + None => { + error!(Source = "Updater"; "Cannot check updates: main window not available."); + return Json(CheckUpdateResponse { update_is_available: false, - error: false, + error: true, new_version: String::from(""), changelog: String::from(""), - }) - }, - }, - + }); + } + } + }; + let response = match app_handle.updater() { + Ok(updater) => updater.check().await, Err(e) => { - warn!(Source = "Updater"; "Failed to check for updates: {e}."); + warn!(Source = "Updater"; "Failed to get updater instance: {e}"); + return Json(CheckUpdateResponse { + update_is_available: false, + error: true, + new_version: String::from(""), + changelog: String::from(""), + }); + } + }; + + match response { + Ok(Some(update)) => { + let body_len = update.body.as_ref().map_or(0, |body| body.len()); + let date = update.date; + let new_version = update.version.clone(); + info!(Source = "Tauri"; "Updater: update available: body size={body_len} time={date:?} version={new_version}"); + let changelog = update.body.clone().unwrap_or_default(); + *CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update); + Json(CheckUpdateResponse { + update_is_available: true, + error: false, + new_version, + changelog, + }) + } + Ok(None) => { + info!(Source = "Tauri"; "Updater: app is already up to date"); + Json(CheckUpdateResponse { + update_is_available: false, + error: false, + new_version: String::from(""), + changelog: String::from(""), + }) + } + Err(e) => { + warn!(Source = "Tauri"; "Updater: failed to update: {e}"); Json(CheckUpdateResponse { update_is_available: false, error: true, new_version: String::from(""), changelog: String::from(""), }) - }, + } } } @@ -463,9 +510,51 @@ pub async fn install_update(_token: APIToken) { } let cloned_response_option = CHECK_UPDATE_RESPONSE.lock().unwrap().clone(); + let app_handle = MAIN_WINDOW + .lock() + .unwrap() + .as_ref() + .map(|window| window.app_handle().clone()); + match cloned_response_option { Some(update_response) => { - update_response.download_and_install().await.unwrap(); + info!(Source = "Tauri"; "Updater: update is pending!"); + let result = update_response.download_and_install( + |chunk_length, _content_length| { + trace!(Source = "Tauri"; "Updater: downloading chunk of {chunk_length} bytes"); + }, + || { + info!(Source = "Tauri"; "Updater: update has been downloaded!"); + warn!(Source = "Tauri"; "Try to stop the .NET server now..."); + + if is_prod() { + stop_dotnet_server(); + stop_qdrant_server(); + } else { + warn!(Source = "Tauri"; "Development environment detected; do not stop the .NET server."); + } + }, + ).await; + + match result { + Ok(_) => { + info!(Source = "Tauri"; "Updater: app has been updated"); + warn!(Source = "Tauri"; "Try to restart the app now..."); + + if is_prod() { + if let Some(handle) = app_handle { + handle.restart(); + } else { + warn!(Source = "Tauri"; "Cannot restart after update: main window not available."); + } + } else { + warn!(Source = "Tauri"; "Development environment detected; do not restart the app."); + } + } + Err(e) => { + warn!(Source = "Tauri"; "Updater: failed to update: {e}"); + } + } }, None => { @@ -474,269 +563,6 @@ pub async fn install_update(_token: APIToken) { } } -/// Let the user select a directory. -#[post("/select/directory?", data = "<previous_directory>")] -pub fn select_directory( - _token: APIToken, - title: &str, - previous_directory: Option<Json<PreviousDirectory>>, -) -> Json<DirectorySelectionResponse> { - let folder_path = match previous_directory { - Some(previous) => { - let previous_path = previous.path.as_str(); - create_file_dialog() - .set_title(title) - .set_directory(previous_path) - .pick_folder() - }, - - None => create_file_dialog().set_title(title).pick_folder(), - }; - - match folder_path { - Some(path) => { - info!("User selected directory: {path:?}"); - Json(DirectorySelectionResponse { - user_cancelled: false, - selected_directory: path.to_str().unwrap().to_string(), - }) - }, - - None => { - info!("User cancelled directory selection."); - Json(DirectorySelectionResponse { - user_cancelled: true, - selected_directory: String::from(""), - }) - }, - } -} - -#[derive(Clone, Deserialize)] -pub struct PreviousDirectory { - path: String, -} - -#[derive(Clone, Deserialize)] -pub struct FileTypeFilter { - filter_name: String, - filter_extensions: Vec<String>, -} - -#[derive(Clone, Deserialize)] -pub struct SelectFileOptions { - title: String, - previous_file: Option<PreviousFile>, - filter: Option<FileTypeFilter>, -} - -#[derive(Clone, Deserialize)] -pub struct SaveFileOptions { - title: String, - name_file: Option<PreviousFile>, - filter: Option<FileTypeFilter>, -} - -#[derive(Serialize)] -pub struct DirectorySelectionResponse { - user_cancelled: bool, - selected_directory: String, -} - -/// Let the user select a file. -#[post("/select/file", data = "<payload>")] -pub fn select_file( - _token: APIToken, - payload: Json<SelectFileOptions>, -) -> Json<FileSelectionResponse> { - // Create a new file dialog builder: - let file_dialog = create_file_dialog(); - - // Set the title of the file dialog: - let file_dialog = file_dialog.set_title(&payload.title); - - // Set the file type filter if provided: - let file_dialog = apply_filter(file_dialog, &payload.filter); - - // Set the previous file path if provided: - let file_dialog = match &payload.previous_file { - Some(previous) => { - let previous_path = previous.file_path.as_str(); - file_dialog.set_directory(previous_path) - }, - - None => file_dialog, - }; - - // Show the file dialog and get the selected file path: - let file_path = file_dialog.pick_file(); - match file_path { - Some(path) => { - info!("User selected file: {path:?}"); - Json(FileSelectionResponse { - user_cancelled: false, - selected_file_path: path.to_str().unwrap().to_string(), - }) - }, - - None => { - info!("User cancelled file selection."); - Json(FileSelectionResponse { - user_cancelled: true, - selected_file_path: String::from(""), - }) - }, - } -} - -/// Let the user select some files. -#[post("/select/files", data = "<payload>")] -pub fn select_files( - _token: APIToken, - payload: Json<SelectFileOptions>, -) -> Json<FilesSelectionResponse> { - // Create a new file dialog builder: - let file_dialog = create_file_dialog(); - - // Set the title of the file dialog: - let file_dialog = file_dialog.set_title(&payload.title); - - // Set the file type filter if provided: - let file_dialog = apply_filter(file_dialog, &payload.filter); - - // Set the previous file path if provided: - let file_dialog = match &payload.previous_file { - Some(previous) => { - let previous_path = previous.file_path.as_str(); - file_dialog.set_directory(previous_path) - }, - - None => file_dialog, - }; - - // Show the file dialog and get the selected file path: - let file_paths = file_dialog.pick_files(); - match file_paths { - Some(paths) => { - info!("User selected {} files.", paths.len()); - Json(FilesSelectionResponse { - user_cancelled: false, - selected_file_paths: paths - .iter() - .map(|p| p.to_str().unwrap().to_string()) - .collect(), - }) - } - - None => { - info!("User cancelled file selection."); - Json(FilesSelectionResponse { - user_cancelled: true, - selected_file_paths: Vec::new(), - }) - }, - } -} - -#[post("/save/file", data = "<payload>")] -pub fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json<FileSaveResponse> { - // Create a new file dialog builder: - let file_dialog = create_file_dialog(); - - // Set the title of the file dialog: - let file_dialog = file_dialog.set_title(&payload.title); - - // Set the file type filter if provided: - let file_dialog = apply_filter(file_dialog, &payload.filter); - - // Set the previous file path if provided: - let file_dialog = match &payload.name_file { - Some(previous) => { - let previous_path = previous.file_path.as_str(); - file_dialog.set_directory(previous_path) - }, - - None => file_dialog, - }; - - // Displays the file dialogue box and select the file: - let file_path = file_dialog.save_file(); - match file_path { - Some(path) => { - info!("User selected file for writing operation: {path:?}"); - Json(FileSaveResponse { - user_cancelled: false, - save_file_path: path.to_str().unwrap().to_string(), - }) - }, - - None => { - info!("User cancelled file selection."); - Json(FileSaveResponse { - user_cancelled: true, - save_file_path: String::from(""), - }) - }, - } -} - -#[derive(Clone, Deserialize)] -pub struct PreviousFile { - file_path: String, -} - -/// Creates a file dialog builder and assigns the main window as parent where supported. -fn create_file_dialog() -> FileDialogBuilder { - let file_dialog = FileDialogBuilder::new(); - - #[cfg(any(windows, target_os = "macos"))] - { - let main_window_lock = MAIN_WINDOW.lock().unwrap(); - match main_window_lock.as_ref() { - Some(window) => file_dialog.set_parent(window), - None => { - warn!(Source = "Tauri"; "Cannot assign parent window to file dialog: main window not available."); - file_dialog - } - } - } - - #[cfg(not(any(windows, target_os = "macos")))] - { - file_dialog - } -} - -/// Applies an optional file type filter to a FileDialogBuilder. -fn apply_filter(file_dialog: FileDialogBuilder, filter: &Option<FileTypeFilter>) -> FileDialogBuilder { - match filter { - Some(f) => file_dialog.add_filter( - &f.filter_name, - &f.filter_extensions.iter().map(|s| s.as_str()).collect::<Vec<&str>>(), - ), - - None => file_dialog, - } -} - -#[derive(Serialize)] -pub struct FileSelectionResponse { - user_cancelled: bool, - selected_file_path: String, -} - -#[derive(Serialize)] -pub struct FilesSelectionResponse { - user_cancelled: bool, - selected_file_paths: Vec<String>, -} - -#[derive(Serialize)] -pub struct FileSaveResponse { - user_cancelled: bool, - save_file_path: String, -} - /// Request payload for registering a global shortcut. #[derive(Clone, Deserialize)] pub struct RegisterShortcutRequest { @@ -765,47 +591,42 @@ pub struct AppExitResponse { /// Internal helper function to register a shortcut with its callback. /// This is used by both `register_shortcut` and `resume_shortcuts` to /// avoid code duplication. -fn register_shortcut_with_callback( - shortcut_manager: &mut impl GlobalShortcutManager, +fn register_shortcut_with_callback<R: tauri::Runtime>( + app_handle: &tauri::AppHandle<R>, shortcut: &str, shortcut_id: Shortcut, event_sender: broadcast::Sender<Event>, -) -> Result<(), tauri::Error> { - // - // Match the shortcut registration to transform the Tauri result into the Rust result: - // - match shortcut_manager.register(shortcut, move || { +) -> Result<(), tauri_plugin_global_shortcut::Error> { + let shortcut_manager = app_handle.global_shortcut(); + shortcut_manager.on_shortcut(shortcut, move |_app, _shortcut, _event| { info!(Source = "Tauri"; "Global shortcut triggered for '{}'.", shortcut_id); let event = Event::new(TauriEventType::GlobalShortcutPressed, vec![shortcut_id.to_string()]); let sender = event_sender.clone(); tauri::async_runtime::spawn(async move { - match sender.send(event) { - Ok(_) => {} - Err(error) => error!(Source = "Tauri"; "Failed to send global shortcut event: {error}"), + if let Err(error) = sender.send(event) { + error!(Source = "Tauri"; "Failed to send global shortcut event: {error}"); } }); - }) { - Ok(_) => Ok(()), - Err(e) => Err(e.into()), - } + }) } /// Requests a controlled shutdown of the entire desktop application. #[post("/app/exit")] pub fn exit_app(_token: APIToken) -> Json<AppExitResponse> { - let main_window_lock = MAIN_WINDOW.lock().unwrap(); - let main_window = match main_window_lock.as_ref() { - Some(window) => window, - None => { - error!(Source = "Tauri"; "Cannot exit app: main window not available."); - return Json(AppExitResponse { - success: false, - error_message: "Main window not available".to_string(), - }); + let app_handle = { + let main_window_lock = MAIN_WINDOW.lock().unwrap(); + match main_window_lock.as_ref() { + Some(window) => window.app_handle().clone(), + None => { + error!(Source = "Tauri"; "Cannot exit app: main window not available."); + return Json(AppExitResponse { + success: false, + error_message: "Main window not available".to_string(), + }); + } } }; - let app_handle = main_window.app_handle(); info!(Source = "Tauri"; "Controlled app exit was requested by the UI."); tauri::async_runtime::spawn(async move { time::sleep(Duration::from_millis(50)).await; @@ -848,7 +669,8 @@ pub fn register_shortcut(_token: APIToken, payload: Json<RegisterShortcutRequest } }; - let mut shortcut_manager = main_window.app_handle().global_shortcut_manager(); + let app_handle = main_window.app_handle(); + let shortcut_manager = app_handle.global_shortcut(); let mut registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap(); // Unregister the old shortcut if one exists for this name: @@ -887,7 +709,7 @@ pub fn register_shortcut(_token: APIToken, payload: Json<RegisterShortcutRequest drop(event_broadcast_lock); // Register the new shortcut: - match register_shortcut_with_callback(&mut shortcut_manager, &new_shortcut, id, event_sender) { + match register_shortcut_with_callback(app_handle, &new_shortcut, id, event_sender) { Ok(_) => { info!(Source = "Tauri"; "Global shortcut '{new_shortcut}' registered successfully for '{}'.", id); registered_shortcuts.insert(id, new_shortcut); @@ -997,7 +819,8 @@ pub fn suspend_shortcuts(_token: APIToken) -> Json<ShortcutResponse> { } }; - let mut shortcut_manager = main_window.app_handle().global_shortcut_manager(); + let app_handle = main_window.app_handle(); + let shortcut_manager = app_handle.global_shortcut(); let registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap(); // Unregister all shortcuts from the OS (but keep them in our map): @@ -1033,7 +856,7 @@ pub fn resume_shortcuts(_token: APIToken) -> Json<ShortcutResponse> { } }; - let mut shortcut_manager = main_window.app_handle().global_shortcut_manager(); + let app_handle = main_window.app_handle(); let registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap(); // Get the event broadcast sender for the shortcut callbacks: @@ -1058,7 +881,7 @@ pub fn resume_shortcuts(_token: APIToken) -> Json<ShortcutResponse> { continue; } - match register_shortcut_with_callback(&mut shortcut_manager, shortcut, *shortcut_id, event_sender.clone()) { + match register_shortcut_with_callback(&app_handle, shortcut, *shortcut_id, event_sender.clone()) { Ok(_) => { info!(Source = "Tauri"; "Re-registered shortcut '{shortcut}' for '{}'.", shortcut_id); success_count += 1; @@ -1119,15 +942,31 @@ fn validate_shortcut_syntax(shortcut: &str) -> bool { has_key } -fn set_pdfium_path(path_resolver: PathResolver) { - let pdfium_relative_source_path = String::from("resources/libraries/"); - let pdfium_source_path = path_resolver.resolve_resource(pdfium_relative_source_path); - if pdfium_source_path.is_none() { - error!(Source = "Bootloader Tauri"; "Failed to set the PDFium library path."); - return; - } +fn set_pdfium_path<R: tauri::Runtime>(path_resolver: &PathResolver<R>) { + let resource_dir = match path_resolver.resource_dir() { + Ok(path) => path, + Err(error) => { + error!(Source = "Bootloader Tauri"; "Failed to resolve resource dir: {error}"); + return; + } + }; - let pdfium_source_path = pdfium_source_path.unwrap(); - let pdfium_source_path = pdfium_source_path.to_str().unwrap().to_string(); - *PDFIUM_LIB_PATH.lock().unwrap() = Some(pdfium_source_path.clone()); + let candidate_paths = [ + resource_dir.join("resources").join("libraries"), + resource_dir.join("libraries"), + ]; + + let pdfium_source_path = candidate_paths + .iter() + .find(|path| path.exists()) + .map(|path| path.to_string_lossy().to_string()); + + match pdfium_source_path { + Some(path) => { + *PDFIUM_LIB_PATH.lock().unwrap() = Some(path); + } + None => { + error!(Source = "Bootloader Tauri"; "Failed to set the PDFium library path."); + } + } } diff --git a/runtime/src/dotnet.rs b/runtime/src/dotnet.rs index 11cc3db5..7cca4599 100644 --- a/runtime/src/dotnet.rs +++ b/runtime/src/dotnet.rs @@ -6,8 +6,9 @@ use base64::prelude::BASE64_STANDARD; use log::{error, info, warn}; use once_cell::sync::Lazy; use rocket::get; -use tauri::api::process::{Command, CommandChild, CommandEvent}; use tauri::Url; +use tauri_plugin_shell::process::{CommandChild, CommandEvent}; +use tauri_plugin_shell::ShellExt; use crate::api_token::APIToken; use crate::runtime_api_token::API_TOKEN; use crate::app_window::change_location_to; @@ -130,14 +131,14 @@ pub fn create_startup_env_file() { } /// Starts the .NET server in a separate process. -pub fn start_dotnet_server() { +pub fn start_dotnet_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>) { // Get the secret password & salt and convert it to a base64 string: let secret_password = BASE64_STANDARD.encode(ENCRYPTION.secret_password); let secret_key_salt = BASE64_STANDARD.encode(ENCRYPTION.secret_key_salt); let api_port = *API_SERVER_PORT; - let dotnet_server_environment = HashMap::from_iter([ + let dotnet_server_environment: HashMap<String, String> = HashMap::from_iter([ (String::from("AI_STUDIO_SECRET_PASSWORD"), secret_password), (String::from("AI_STUDIO_SECRET_KEY_SALT"), secret_key_salt), (String::from("AI_STUDIO_CERTIFICATE_FINGERPRINT"), CERTIFICATE_FINGERPRINT.get().unwrap().to_string()), @@ -148,11 +149,13 @@ pub fn start_dotnet_server() { info!("Try to start the .NET server..."); let server_spawn_clone = DOTNET_SERVER.clone(); tauri::async_runtime::spawn(async move { - let (mut rx, child) = Command::new_sidecar("mindworkAIStudioServer") - .expect("Failed to create sidecar") - .envs(dotnet_server_environment) - .spawn() - .expect("Failed to spawn .NET server process."); + let shell = app_handle.shell(); + let (mut rx, child) = shell + .sidecar("mindworkAIStudioServer") + .expect("Failed to create sidecar") + .envs(dotnet_server_environment) + .spawn() + .expect("Failed to spawn .NET server process."); let server_pid = child.pid(); info!(Source = "Bootloader .NET"; "The .NET server process started with PID={server_pid}."); log_potential_stale_process(Path::new(DATA_DIRECTORY.get().unwrap()).join(PID_FILE_NAME), server_pid, SIDECAR_TYPE); @@ -163,10 +166,13 @@ pub fn start_dotnet_server() { // Log the output of the .NET server: // NOTE: Log events are sent via structured HTTP API calls. // This loop serves for fundamental output (e.g., startup errors). - while let Some(CommandEvent::Stdout(line)) = rx.recv().await { - let line = sanitize_stdout_line(line.trim_end()); - if !line.trim().is_empty() { - info!(Source = ".NET Server (stdout)"; "{line}"); + while let Some(event) = rx.recv().await { + if let CommandEvent::Stdout(line) = event { + let line_utf8 = String::from_utf8_lossy(&line).to_string(); + let line = sanitize_stdout_line(line_utf8.trim_end()); + if !line.trim().is_empty() { + info!(Source = ".NET Server (stdout)"; "{line}"); + } } } }); diff --git a/runtime/src/file_actions.rs b/runtime/src/file_actions.rs new file mode 100644 index 00000000..94eeb629 --- /dev/null +++ b/runtime/src/file_actions.rs @@ -0,0 +1,298 @@ +use log::{error, info}; +use rocket::post; +use rocket::serde::{Deserialize, Serialize}; +use rocket::serde::json::Json; +use tauri_plugin_dialog::{DialogExt, FileDialogBuilder}; +use crate::api_token::APIToken; +use crate::app_window::MAIN_WINDOW; + +#[derive(Clone, Deserialize)] +pub struct PreviousDirectory { + path: String, +} + +#[derive(Clone, Deserialize)] +pub struct FileTypeFilter { + filter_name: String, + filter_extensions: Vec<String>, +} + +#[derive(Clone, Deserialize)] +pub struct SelectFileOptions { + title: String, + previous_file: Option<PreviousFile>, + filter: Option<FileTypeFilter>, +} + +#[derive(Clone, Deserialize)] +pub struct SaveFileOptions { + title: String, + name_file: Option<PreviousFile>, + filter: Option<FileTypeFilter>, +} + +#[derive(Serialize)] +pub struct DirectorySelectionResponse { + user_cancelled: bool, + selected_directory: String, +} + +#[derive(Serialize)] +pub struct FileSelectionResponse { + user_cancelled: bool, + selected_file_path: String, +} + +#[derive(Serialize)] +pub struct FilesSelectionResponse { + user_cancelled: bool, + selected_file_paths: Vec<String>, +} + +#[derive(Serialize)] +pub struct FileSaveResponse { + user_cancelled: bool, + save_file_path: String, +} + +#[derive(Clone, Deserialize)] +pub struct PreviousFile { + file_path: String, +} + +/// Let the user select a directory. +#[post("/select/directory?<title>", data = "<previous_directory>")] +pub fn select_directory( + _token: APIToken, + title: &str, + previous_directory: Option<Json<PreviousDirectory>>, +) -> Json<DirectorySelectionResponse> { + let main_window_lock = MAIN_WINDOW.lock().unwrap(); + let main_window = match main_window_lock.as_ref() { + Some(window) => window, + None => { + error!(Source = "Tauri"; "Cannot open directory dialog: main window not available."); + return Json(DirectorySelectionResponse { + user_cancelled: true, + selected_directory: String::from(""), + }); + } + }; + + let mut dialog = main_window.dialog().file().set_parent(main_window).set_title(title); + if let Some(previous) = previous_directory { + dialog = dialog.set_directory(previous.path.clone()); + } + + drop(main_window_lock); + + let folder_path = dialog.blocking_pick_folder(); + match folder_path { + Some(path) => { + match path.into_path() { + Ok(pb) => { + info!("User selected directory: {pb:?}"); + Json(DirectorySelectionResponse { + user_cancelled: false, + selected_directory: pb.to_string_lossy().to_string(), + }) + } + Err(e) => { + error!(Source = "Tauri"; "Failed to convert directory path: {e}"); + Json(DirectorySelectionResponse { + user_cancelled: true, + selected_directory: String::new(), + }) + } + } + }, + + None => { + info!("User cancelled directory selection."); + Json(DirectorySelectionResponse { + user_cancelled: true, + selected_directory: String::from(""), + }) + }, + } +} + +/// Let the user select a file. +#[post("/select/file", data = "<payload>")] +pub fn select_file( + _token: APIToken, + payload: Json<SelectFileOptions>, +) -> Json<FileSelectionResponse> { + // Create a new file dialog builder: + let file_dialog = MAIN_WINDOW + .lock() + .unwrap() + .as_ref() + .map(|w| w.dialog().file().set_parent(w).set_title(&payload.title)); + + let Some(mut file_dialog) = file_dialog else { + error!(Source = "Tauri"; "Cannot open file dialog: main window not available."); + return Json(FileSelectionResponse { + user_cancelled: true, + selected_file_path: String::from(""), + }); + }; + + // Set the file type filter if provided: + file_dialog = apply_filter(file_dialog, &payload.filter); + + // Set the previous file path if provided: + if let Some(previous) = &payload.previous_file { + let previous_path = previous.file_path.as_str(); + file_dialog = file_dialog.set_directory(previous_path); + } + + // Show the file dialog and get the selected file path: + let file_path = file_dialog.blocking_pick_file(); + match file_path { + Some(path) => match path.into_path() { + Ok(pb) => { + info!("User selected file: {pb:?}"); + Json(FileSelectionResponse { + user_cancelled: false, + selected_file_path: pb.to_string_lossy().to_string(), + }) + } + Err(e) => { + error!(Source = "Tauri"; "Failed to convert file path: {e}"); + Json(FileSelectionResponse { + user_cancelled: true, + selected_file_path: String::new(), + }) + } + }, + + None => { + info!("User cancelled file selection."); + Json(FileSelectionResponse { + user_cancelled: true, + selected_file_path: String::from(""), + }) + }, + } +} + +/// Let the user select some files. +#[post("/select/files", data = "<payload>")] +pub fn select_files( + _token: APIToken, + payload: Json<SelectFileOptions>, +) -> Json<FilesSelectionResponse> { + // Create a new file dialog builder: + let file_dialog = MAIN_WINDOW + .lock() + .unwrap() + .as_ref() + .map(|w| w.dialog().file().set_parent(w).set_title(&payload.title)); + + let Some(mut file_dialog) = file_dialog else { + error!(Source = "Tauri"; "Cannot open file dialog: main window not available."); + return Json(FilesSelectionResponse { + user_cancelled: true, + selected_file_paths: Vec::new(), + }); + }; + + // Set the file type filter if provided: + file_dialog = apply_filter(file_dialog, &payload.filter); + + // Set the previous file path if provided: + if let Some(previous) = &payload.previous_file { + let previous_path = previous.file_path.as_str(); + file_dialog = file_dialog.set_directory(previous_path); + } + + // Show the file dialog and get the selected file path: + let file_paths = file_dialog.blocking_pick_files(); + match file_paths { + Some(paths) => { + let converted: Vec<String> = paths.into_iter().filter_map(|p| p.into_path().ok()).map(|pb| pb.to_string_lossy().to_string()).collect(); + info!("User selected {} files.", converted.len()); + Json(FilesSelectionResponse { + user_cancelled: false, + selected_file_paths: converted, + }) + } + + None => { + info!("User cancelled file selection."); + Json(FilesSelectionResponse { + user_cancelled: true, + selected_file_paths: Vec::new(), + }) + }, + } +} + +#[post("/save/file", data = "<payload>")] +pub fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json<FileSaveResponse> { + // Create a new file dialog builder: + let file_dialog = MAIN_WINDOW + .lock() + .unwrap() + .as_ref() + .map(|w| w.dialog().file().set_parent(w).set_title(&payload.title)); + + let Some(mut file_dialog) = file_dialog else { + error!(Source = "Tauri"; "Cannot open save dialog: main window not available."); + return Json(FileSaveResponse { + user_cancelled: true, + save_file_path: String::from(""), + }); + }; + + // Set the file type filter if provided: + file_dialog = apply_filter(file_dialog, &payload.filter); + + // Set the previous file path if provided: + if let Some(previous) = &payload.name_file { + let previous_path = previous.file_path.as_str(); + file_dialog = file_dialog.set_directory(previous_path); + } + + // Displays the file dialogue box and select the file: + let file_path = file_dialog.blocking_save_file(); + match file_path { + Some(path) => match path.into_path() { + Ok(pb) => { + info!("User selected file for writing operation: {pb:?}"); + Json(FileSaveResponse { + user_cancelled: false, + save_file_path: pb.to_string_lossy().to_string(), + }) + } + Err(e) => { + error!(Source = "Tauri"; "Failed to convert save file path: {e}"); + Json(FileSaveResponse { + user_cancelled: true, + save_file_path: String::new(), + }) + } + }, + + None => { + info!("User cancelled file selection."); + Json(FileSaveResponse { + user_cancelled: true, + save_file_path: String::from(""), + }) + }, + } +} + +/// Applies an optional file type filter to a FileDialogBuilder. +fn apply_filter<R: tauri::Runtime>(file_dialog: FileDialogBuilder<R>, filter: &Option<FileTypeFilter>) -> FileDialogBuilder<R> { + match filter { + Some(f) => file_dialog.add_filter( + &f.filter_name, + &f.filter_extensions.iter().map(|s| s.as_str()).collect::<Vec<&str>>(), + ), + + None => file_dialog, + } +} \ No newline at end of file diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 1b13e099..b36a1505 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -17,4 +17,5 @@ pub mod qdrant; pub mod certificate_factory; pub mod runtime_api_token; pub mod stale_process_cleanup; -mod sidecar_types; \ No newline at end of file +mod sidecar_types; +mod file_actions; \ No newline at end of file diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index dff8814f..11e52005 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -10,15 +10,17 @@ use once_cell::sync::Lazy; use rocket::get; use rocket::serde::json::Json; use rocket::serde::Serialize; -use tauri::api::process::{Command, CommandChild, CommandEvent}; use crate::api_token::{APIToken}; use crate::environment::{is_dev, DATA_DIRECTORY}; use crate::certificate_factory::generate_certificate; use std::path::PathBuf; -use tauri::PathResolver; +use tauri::Manager; +use tauri::path::BaseDirectory; use tempfile::{TempDir, Builder}; use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process}; use crate::sidecar_types::SidecarType; +use tauri_plugin_shell::process::{CommandChild, CommandEvent}; +use tauri_plugin_shell::ShellExt; // Qdrant server process started in a separate process and can communicate // via HTTP or gRPC with the .NET server and the runtime process @@ -98,7 +100,7 @@ pub fn qdrant_port(_token: APIToken) -> Json<ProvideQdrantInfo> { } /// Starts the Qdrant server in a separate process. -pub fn start_qdrant_server(path_resolver: PathResolver){ +pub fn start_qdrant_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>){ let path = qdrant_base_path(); if !path.exists() { if let Err(e) = fs::create_dir_all(&path){ @@ -121,7 +123,7 @@ pub fn start_qdrant_server(path_resolver: PathResolver){ let snapshot_path = path.join("snapshots").to_string_lossy().to_string(); let init_path = path.join(".qdrant-initialized").to_string_lossy().to_string(); - let qdrant_server_environment = HashMap::from_iter([ + let qdrant_server_environment: HashMap<String, String> = HashMap::from_iter([ (String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()), (String::from("QDRANT__SERVICE__GRPC_PORT"), QDRANT_SERVER_PORT_GRPC.to_string()), (String::from("QDRANT_INIT_FILE_PATH"), init_path), @@ -135,9 +137,9 @@ pub fn start_qdrant_server(path_resolver: PathResolver){ let server_spawn_clone = QDRANT_SERVER.clone(); let qdrant_relative_source_path = "resources/databases/qdrant/config.yaml"; - let qdrant_source_path = match path_resolver.resolve_resource(qdrant_relative_source_path) { - Some(path) => path, - None => { + let qdrant_source_path = match app_handle.path().resolve(qdrant_relative_source_path, BaseDirectory::Resource) { + Ok(path) => path, + Err(_) => { let reason = format!("The Qdrant config resource '{qdrant_relative_source_path}' could not be resolved."); error!(Source = "Qdrant"; "{reason} Starting the app without Qdrant."); set_qdrant_unavailable(reason); @@ -147,7 +149,9 @@ pub fn start_qdrant_server(path_resolver: PathResolver){ let qdrant_source_path_display = qdrant_source_path.to_string_lossy().to_string(); tauri::async_runtime::spawn(async move { - let sidecar = match Command::new_sidecar("qdrant") { + let shell = app_handle.shell(); + + let sidecar = match shell.sidecar("qdrant") { Ok(sidecar) => sidecar, Err(e) => { let reason = format!("Failed to create sidecar for Qdrant: {e}"); @@ -183,7 +187,8 @@ pub fn start_qdrant_server(path_resolver: PathResolver){ while let Some(event) = rx.recv().await { match event { CommandEvent::Stdout(line) => { - let line = line.trim_end(); + let line_utf8 = String::from_utf8_lossy(&line).to_string(); + let line = line_utf8.trim_end(); if line.contains("INFO") || line.contains("info") { info!(Source = "Qdrant Server"; "{line}"); } else if line.contains("WARN") || line.contains("warning") { @@ -196,7 +201,8 @@ pub fn start_qdrant_server(path_resolver: PathResolver){ }, CommandEvent::Stderr(line) => { - error!(Source = "Qdrant Server (stderr)"; "{line}"); + let line_utf8 = String::from_utf8_lossy(&line).to_string(); + error!(Source = "Qdrant Server (stderr)"; "{line_utf8}"); }, _ => {} diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index aa743345..4d881fe3 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -72,11 +72,11 @@ pub fn start_runtime_api() { crate::app_window::get_event_stream, crate::app_window::check_for_update, crate::app_window::install_update, - crate::app_window::select_directory, - crate::app_window::select_file, - crate::app_window::select_files, - crate::app_window::save_file, crate::app_window::exit_app, + crate::file_actions::select_directory, + crate::file_actions::select_file, + crate::file_actions::select_files, + crate::file_actions::save_file, crate::secret::get_secret, crate::secret::store_secret, crate::secret::delete_secret, diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index 46a7c4f2..08a7a640 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -1,44 +1,52 @@ { + "productName": "MindWork AI Studio", + "mainBinaryName": "MindWork AI Studio", + "version": "26.5.2", + "identifier": "com.github.mindwork-ai.ai-studio", + "build": { - "devPath": "ui/", - "distDir": "ui/", - "withGlobalTauri": false + "frontendDist": "ui/" }, - "package": { - "productName": "MindWork AI Studio", - "version": "26.5.1" - }, - "tauri": { - "allowlist": { - "all": false, - "shell": { - "sidecar": true, - "all": false, - "open": true, - "scope": [ - { - "name": "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer", - "sidecar": true, - "args": true - }, - { - "name": "target/databases/qdrant/qdrant", - "sidecar": true, - "args": true - } - ] - }, - "http" : { - "all": true, - "request": true, - "scope": [ - "http://localhost" - ] - }, - "fs": { - "scope": ["$RESOURCE/resources/*"] - } + + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "externalBin": [ + "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer", + "target/databases/qdrant/qdrant" + ], + "resources": [ + "resources/databases/qdrant/config.yaml", + "resources/libraries/*" + ], + "macOS": { + "exceptionDomain": "localhost" }, + "createUpdaterArtifacts": "v1Compatible" + }, + + "plugins": { + "updater": { + "windows": { + "installMode": "passive" + }, + "endpoints": [ + "https://github.com/MindWorkAI/AI-Studio/releases/download/v26.5.3/latest.json" + ], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDM3MzE4MTM4RTNDMkM0NEQKUldSTnhNTGpPSUV4TjFkczFxRFJOZWgydzFQN1dmaFlKbXhJS1YyR1RKS1RnR09jYUpMaGsrWXYK" + } + }, + + "app": { + "withGlobalTauri": false, + "windows": [ { "fullscreen": false, @@ -46,51 +54,13 @@ "title": "MindWork AI Studio", "width": 1920, "height": 1080, - "fileDropEnabled": true + "dragDropEnabled": true, + "useHttpsScheme": true } ], + "security": { - "csp": null, - "dangerousRemoteDomainIpcAccess": [ - { - "domain": "localhost", - "windows": ["main"], - "enableTauriAPI": true - } - ] - }, - "bundle": { - "active": true, - "targets": "all", - "identifier": "com.github.mindwork-ai.ai-studio", - "externalBin": [ - "../app/MindWork AI Studio/bin/dist/mindworkAIStudioServer", - "target/databases/qdrant/qdrant" - ], - "resources": [ - "resources/**" - ], - "macOS": { - "exceptionDomain": "localhost" - }, - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ] - }, - "updater": { - "active": true, - "endpoints": [ - "https://github.com/MindWorkAI/AI-Studio/releases/download/v26.5.2/latest.json" - ], - "dialog": false, - "windows": { - "installMode": "passive" - }, - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDM3MzE4MTM4RTNDMkM0NEQKUldSTnhNTGpPSUV4TjFkczFxRFJOZWgydzFQN1dmaFlKbXhJS1YyR1RKS1RnR09jYUpMaGsrWXYK" + "csp": null } } } From db4382d673f8baa860d85a92dc59e5f95c3ba819 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Wed, 6 May 2026 19:12:19 +0200 Subject: [PATCH 03/21] Updated CI/CD pipeline (#751) --- .github/workflows/build-and-release.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 00e20baa..3b4f491e 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -704,19 +704,19 @@ jobs: if: matrix.platform == 'ubuntu-22.04' && contains(matrix.rust_target, 'x86_64') run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 + sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils - name: Setup dependencies (Ubuntu-specific, ARM) if: matrix.platform == 'ubuntu-22.04-arm' && contains(matrix.rust_target, 'aarch64') run: | sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 + sudo apt-get install -y libwebkit2gtk-4.1-dev libayatana-appindicator3-dev librsvg2-dev patchelf libfuse2 xdg-utils - name: Setup Tauri (Unix) if: matrix.platform != 'windows-latest' run: | if ! cargo tauri --version 2>/dev/null | grep -Eq '^tauri-cli 2\.'; then - cargo install tauri-cli --version "^2.0.0" --locked --force + cargo install tauri-cli --version "^2.11.0" --locked --force else echo "Tauri CLI v2 is already installed" fi @@ -726,7 +726,7 @@ jobs: run: | $tauriVersion = cargo tauri --version 2>$null if (-not $tauriVersion -or $tauriVersion -notmatch '^tauri-cli 2\.') { - cargo install tauri-cli --version "^2.0.0" --locked --force + cargo install tauri-cli --version "^2.11.0" --locked --force } else { Write-Output "Tauri CLI v2 is already installed" } From 666956a7e405d1fc01fcf328a28c1384e5e9926c Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Wed, 6 May 2026 19:40:54 +0200 Subject: [PATCH 04/21] Removed support for deb release targets (#752) --- .github/workflows/build-and-release.yml | 31 +++++++------------------ runtime/tauri.conf.json | 6 ++++- 2 files changed, 13 insertions(+), 24 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 3b4f491e..422a40a2 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -234,15 +234,15 @@ jobs: rust_target: 'x86_64-unknown-linux-gnu' dotnet_runtime: 'linux-x64' dotnet_name_postfix: '-x86_64-unknown-linux-gnu' - tauri_bundle: 'appimage,deb,updater' - tauri_bundle_pr: 'appimage,deb' + tauri_bundle: 'appimage,updater' + tauri_bundle_pr: 'appimage' - platform: 'ubuntu-22.04-arm' # for ARM-based Linux rust_target: 'aarch64-unknown-linux-gnu' dotnet_runtime: 'linux-arm64' dotnet_name_postfix: '-aarch64-unknown-linux-gnu' - tauri_bundle: 'appimage,deb,updater' - tauri_bundle_pr: 'appimage,deb' + tauri_bundle: 'appimage,updater' + tauri_bundle_pr: 'appimage' - platform: 'windows-latest' # for x86-based Windows rust_target: 'x86_64-pc-windows-msvc' @@ -749,16 +749,11 @@ jobs: rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio_*.exe" -ErrorAction SilentlyContinue rm -Force "runtime/target/${{ matrix.rust_target }}/release/bundle/nsis/MindWork AI Studio*nsis.zip*" -ErrorAction SilentlyContinue - - name: Delete previous artifact, which may exist due to caching (Linux - Debian Package) - if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'deb') - run: | - rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/deb/mind-work-ai-studio_*.deb - - name: Delete previous artifact, which may exist due to caching (Linux - AppImage) if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage') run: | - rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio_*.AppImage - rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio*AppImage.tar.gz* + rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/*.AppImage + rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/*.AppImage.tar.gz* - name: Build Tauri project (Unix) if: matrix.platform != 'windows-latest' @@ -831,24 +826,14 @@ jobs: if-no-files-found: error retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }} - - name: Upload artifact (Linux - Debian Package) - if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'deb') - uses: actions/upload-artifact@v4 - with: - name: MindWork AI Studio (Linux - deb ${{ matrix.dotnet_runtime }}) - path: | - runtime/target/${{ matrix.rust_target }}/release/bundle/deb/mind-work-ai-studio_*.deb - if-no-files-found: error - retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }} - - name: Upload artifact (Linux - AppImage) if: startsWith(matrix.platform, 'ubuntu') && contains(matrix.tauri_bundle, 'appimage') uses: actions/upload-artifact@v4 with: name: MindWork AI Studio (Linux - AppImage ${{ matrix.dotnet_runtime }}) path: | - runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio_*.AppImage - runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/mind-work-ai-studio*AppImage.tar.gz* + runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/*.AppImage + runtime/target/${{ matrix.rust_target }}/release/bundle/appimage/*.AppImage.tar.gz* if-no-files-found: error retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }} diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index 08a7a640..6bfae9c6 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -10,7 +10,11 @@ "bundle": { "active": true, - "targets": "all", + "targets": [ + "appimage", + "dmg", + "nsis" + ], "icon": [ "icons/32x32.png", "icons/128x128.png", From da4d44461f57933e4a60cf262777d8093aa69d6d Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Thu, 7 May 2026 21:22:52 +0200 Subject: [PATCH 05/21] Fixed macOS artifact handling & added platform checks (#753) --- .github/workflows/build-and-release.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 422a40a2..ca343607 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -735,7 +735,7 @@ jobs: if: startsWith(matrix.platform, 'macos') run: | rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/dmg/MindWork AI Studio_*.dmg - rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/macos/MindWork AI Studio.app.tar.gz* + rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/macos/*.app.tar.gz* - name: Delete previous artifact, which may exist due to caching (Windows - MSI) if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'msi') @@ -800,7 +800,7 @@ jobs: name: MindWork AI Studio (macOS ${{ matrix.dotnet_runtime }}) path: | runtime/target/${{ matrix.rust_target }}/release/bundle/dmg/MindWork AI Studio_*.dmg - runtime/target/${{ matrix.rust_target }}/release/bundle/macos/MindWork AI Studio.app.tar.gz* + runtime/target/${{ matrix.rust_target }}/release/bundle/macos/*.app.tar.gz* if-no-files-found: error retention-days: ${{ fromJSON(needs.determine_run_mode.outputs.artifact_retention_days) }} @@ -993,6 +993,13 @@ jobs: exit 1 fi + for platform in darwin-aarch64 darwin-x86_64 linux-aarch64 linux-x86_64 windows-aarch64 windows-x86_64; do + if ! jq -e --arg platform "$platform" '.platforms[$platform]' $GITHUB_WORKSPACE/release/assets/latest.json > /dev/null; then + echo "The generated latest.json is missing platform '$platform'." + exit 1 + fi + done + - name: Show all release assets run: ls -Rlhat $GITHUB_WORKSPACE/release/assets From eb9c6be16e4e065dac3fe522f7425f641e96c849 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Fri, 8 May 2026 09:32:20 +0200 Subject: [PATCH 06/21] Fixed macOS artifact handling (#754) --- .github/workflows/build-and-release.yml | 27 +++++++++++++++++++++---- runtime/tauri.conf.json | 1 + 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index ca343607..3bcbc09f 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -220,14 +220,14 @@ jobs: rust_target: 'aarch64-apple-darwin' dotnet_runtime: 'osx-arm64' dotnet_name_postfix: '-aarch64-apple-darwin' - tauri_bundle: 'dmg,updater' + tauri_bundle: 'dmg,app,updater' tauri_bundle_pr: 'dmg' - platform: 'macos-latest' # for Intel-based macOS rust_target: 'x86_64-apple-darwin' dotnet_runtime: 'osx-x64' dotnet_name_postfix: '-x86_64-apple-darwin' - tauri_bundle: 'dmg,updater' + tauri_bundle: 'dmg,app,updater' tauri_bundle_pr: 'dmg' - platform: 'ubuntu-22.04' # for x86-based Linux @@ -734,8 +734,17 @@ jobs: - name: Delete previous artifact, which may exist due to caching (macOS) if: startsWith(matrix.platform, 'macos') run: | - rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/dmg/MindWork AI Studio_*.dmg - rm -f runtime/target/${{ matrix.rust_target }}/release/bundle/macos/*.app.tar.gz* + dmg_dir="runtime/target/${{ matrix.rust_target }}/release/bundle/dmg" + macos_dir="runtime/target/${{ matrix.rust_target }}/release/bundle/macos" + + if [ -d "$dmg_dir" ]; then + find "$dmg_dir" -maxdepth 1 -name 'MindWork AI Studio_*.dmg' -delete + fi + + if [ -d "$macos_dir" ]; then + find "$macos_dir" -maxdepth 1 -name '*.app' -exec rm -rf {} + + find "$macos_dir" -maxdepth 1 -name '*.app.tar.gz*' -delete + fi - name: Delete previous artifact, which may exist due to caching (Windows - MSI) if: startsWith(matrix.platform, 'windows') && contains(matrix.tauri_bundle, 'msi') @@ -773,6 +782,16 @@ jobs: cd runtime cargo tauri build --target ${{ matrix.rust_target }} --bundles "$bundles" + + if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" != "true" ] && [[ "${{ matrix.platform }}" == macos* ]]; then + app_update_archive_count=$(find target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name '*.app.tar.gz' | wc -l) + app_update_signature_count=$(find target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name '*.app.tar.gz.sig' | wc -l) + + if [ "$app_update_archive_count" -eq 0 ] || [ "$app_update_signature_count" -eq 0 ]; then + echo "Expected macOS updater artifacts were not generated." + exit 1 + fi + fi - name: Build Tauri project (Windows) if: matrix.platform == 'windows-latest' diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index 6bfae9c6..88e11f70 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -12,6 +12,7 @@ "active": true, "targets": [ "appimage", + "app", "dmg", "nsis" ], From f69186f7a9f77da7dffb8358f55995ed4cc37cc7 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Fri, 8 May 2026 13:28:18 +0200 Subject: [PATCH 07/21] Fixed navigation logic for Windows builds (#755) --- runtime/src/app_window.rs | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index d33fdc52..d53a2f6d 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -203,6 +203,14 @@ fn is_local_host(host: Option<&str>) -> bool { matches!(host, Some("localhost") | Some("127.0.0.1") | Some("::1") | Some("[::1]")) } +fn is_tauri_asset_host(host: Option<&str>) -> bool { + matches!(host, Some("tauri.localhost")) +} + +fn is_tauri_asset_url(url: &tauri::Url) -> bool { + matches!(url.scheme(), "http" | "https") && is_tauri_asset_host(url.host_str()) +} + fn is_local_http_url(url: &tauri::Url) -> bool { matches!(url.scheme(), "http" | "https") && is_local_host(url.host_str()) } @@ -220,6 +228,10 @@ fn should_open_in_system_browser<R: tauri::Runtime>(webview: &tauri::Webview<R>, _ => return false, } + if is_tauri_asset_url(url) { + return false; + } + if let Some(approved_app_url) = APPROVED_APP_URL.lock().unwrap().as_ref() { if same_origin(approved_app_url, url) { return false; @@ -942,6 +954,36 @@ fn validate_shortcut_syntax(shortcut: &str) -> bool { has_key } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tauri_localhost_is_tauri_asset_url() { + let https_url = tauri::Url::parse("https://tauri.localhost/index.html").unwrap(); + let http_url = tauri::Url::parse("http://tauri.localhost/index.html").unwrap(); + + assert!(is_tauri_asset_url(&https_url)); + assert!(is_tauri_asset_url(&http_url)); + } + + #[test] + fn localhost_app_url_is_not_tauri_asset_url() { + let url = tauri::Url::parse("http://localhost:12345/").unwrap(); + + assert!(!is_tauri_asset_url(&url)); + assert!(is_local_http_url(&url)); + } + + #[test] + fn external_url_is_not_internal_url() { + let url = tauri::Url::parse("https://example.com/").unwrap(); + + assert!(!is_tauri_asset_url(&url)); + assert!(!is_local_http_url(&url)); + } +} + fn set_pdfium_path<R: tauri::Runtime>(path_resolver: &PathResolver<R>) { let resource_dir = match path_resolver.resource_dir() { Ok(path) => path, From d69eab8807249356bb600312b84c4bf61ed8b4b5 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Tue, 12 May 2026 20:31:08 +0200 Subject: [PATCH 08/21] Migrated to axum (#757) --- .../Assistants/I18N/allTexts.lua | 12 +- .../Pages/Information.razor | 6 +- .../plugin.lua | 12 +- .../plugin.lua | 12 +- .../Tools/Services/RustService.FileSystem.cs | 3 +- .../Tools/Services/RustService.Retrieval.cs | 9 + runtime/Cargo.lock | 794 ++++-------------- runtime/Cargo.toml | 5 +- runtime/src/app_window.rs | 108 ++- runtime/src/clipboard.rs | 7 +- runtime/src/dotnet.rs | 5 +- runtime/src/encryption.rs | 31 +- runtime/src/environment.rs | 24 +- runtime/src/file_actions.rs | 27 +- runtime/src/file_data.rs | 128 ++- runtime/src/log.rs | 29 +- runtime/src/main.rs | 2 - runtime/src/qdrant.rs | 8 +- runtime/src/runtime_api.rs | 154 ++-- runtime/src/runtime_api_token.rs | 32 +- runtime/src/secret.rs | 12 +- 21 files changed, 488 insertions(+), 932 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 07569e09..2c340b17 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -6028,9 +6028,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is -- Encryption secret: is configured UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Encryption secret: is configured" --- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1943216839"] = "We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust." - -- Copies the following to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the following to the clipboard" @@ -6133,6 +6130,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Have feature ide -- Hide Details UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Hide Details" +-- Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3208719461"] = "Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface." + +-- Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3239817808"] = "Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running." + -- Update Pandoc UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3249965383"] = "Update Pandoc" @@ -6226,6 +6229,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T836298648"] = "Provided by confi -- We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate." +-- Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T864851737"] = "Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running." + -- For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T870640199"] = "For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose." diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index 3170be0f..244e8f3e 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -279,7 +279,9 @@ <ThirdPartyComponent Name="Rust" Developer="Graydon Hoare, Rust Foundation, Rust developers & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-lang/rust/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-lang/rust" UseCase="@T("The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software.")"/> <ThirdPartyComponent Name="Tauri" Developer="Daniel Thompson-Yvetot, Lucas Nogueira, Tensor, Boscop, Serge Zaitsev, George Burton & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tauri-apps/tauri/blob/dev/LICENSE_MIT" RepositoryUrl="https://github.com/tauri-apps/tauri" UseCase="@T("Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!")"/> <ThirdPartyComponent Name="Qdrant" Developer="Andrey Vasnetsov, Tim Visée, Arnaud Gourlay, Luis Cossío, Ivan Pleshkov, Roman Titov, xzfc, JojiiOfficial & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://github.com/qdrant/qdrant/blob/master/LICENSE" RepositoryUrl="https://github.com/qdrant/qdrant" UseCase="@T("Qdrant is a vector database and vector similarity search engine. We use it to realize local RAG—retrieval-augmented generation—within AI Studio. Thanks for the effort and great work that has been and is being put into Qdrant.")"/> - <ThirdPartyComponent Name="Rocket" Developer="Sergio Benitez & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rwf2/Rocket/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rwf2/Rocket" UseCase="@T("We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust.")"/> + <ThirdPartyComponent Name="axum" Developer="David Pedersen, Jonas Platte, tottoto, David Mládek, Yann Simon, Tobias Bieniek, Open Source Community & Tokio Project" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/axum/blob/main/LICENSE" RepositoryUrl="https://github.com/tokio-rs/axum" UseCase="@T("Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running.")"/> + <ThirdPartyComponent Name="axum-server" Developer="Eray Karatay, Adi Salimgereyev, daxpedda & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/programatik29/axum-server/blob/master/LICENSE" RepositoryUrl="https://github.com/programatik29/axum-server" UseCase="@T("Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface.")"/> + <ThirdPartyComponent Name="Rustls" Developer="Joe Birr-Pixton, Dirkjan Ochtman, Daniel McCarney, Brian Smith, Jacob Hoffman-Andrews, Jorge Aparicio & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rustls/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/rustls/rustls" UseCase="@T("Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running.")"/> <ThirdPartyComponent Name="serde" Developer="Erick Tryzelaar, David Tolnay & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/serde-rs/serde/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/serde-rs/serde" UseCase="@T("Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json.")"/> <ThirdPartyComponent Name="strum_macros" Developer="Peter Glotfelty & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/Peternator7/strum/blob/master/LICENSE" RepositoryUrl="https://github.com/Peternator7/strum" UseCase="@T("This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems.")"/> <ThirdPartyComponent Name="keyring" Developer="Walther Chen, Daniel Brotsky & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/hwchen/keyring-rs/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/hwchen/keyring-rs" UseCase="@T("In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library.")"/> @@ -312,4 +314,4 @@ </ExpansionPanel> </MudExpansionPanels> </InnerScrolling> -</div> \ No newline at end of file +</div> diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index b65b6552..b74ec6a3 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -6030,9 +6030,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "Diese Bibliothek -- Encryption secret: is configured UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Geheimnis für die Verschlüsselung: ist konfiguriert" --- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1943216839"] = "Wir verwenden Rocket zur Implementierung der Runtime-API. Dies ist notwendig, da die Runtime mit der Benutzeroberfläche (IPC) kommunizieren muss. Rocket ist ein ausgezeichnetes Framework zur Umsetzung von Web-APIs in Rust." - -- Copies the following to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Kopiert Folgendes in die Zwischenablage" @@ -6135,6 +6132,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Haben Sie Ideen -- Hide Details UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Details ausblenden" +-- Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3208719461"] = "Der Axum-Server führt den internen Axum-Dienst über eine sichere lokale Verbindung aus. Dadurch kann AI Studio die Kommunikation zwischen der Rust-Laufzeitumgebung und der Benutzeroberfläche schützen." + +-- Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3239817808"] = "Rustls hilft dabei, die interne Verbindung zwischen der Benutzeroberfläche der App und der Rust-Laufzeitumgebung abzusichern. Dadurch wird die lokale Kommunikation geschützt, die AI Studio während der Ausführung benötigt." + -- Update Pandoc UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3249965383"] = "Pandoc aktualisieren" @@ -6228,6 +6231,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T836298648"] = "Bereitgestellt vo -- We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "Wir verwenden diese Bibliothek, um PowerPoint-Dateien lesen zu können. So ist es möglich, Inhalte aus Folien in Prompts einzufügen und PowerPoint-Dateien in RAG-Prozessen zu berücksichtigen. Wir danken Nils Kruthoff für seine Arbeit an diesem Rust-Crate." +-- Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T864851737"] = "Axum wird verwendet, um den kleinen internen Dienst bereitzustellen, der die Rust-Laufzeitumgebung mit der Benutzeroberfläche der App verbindet. So können beide Teile von AI Studio Informationen austauschen, während die App läuft." + -- For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T870640199"] = "Für einige Datenübertragungen müssen wir die Daten in Base64 kodieren. Diese Rust-Bibliothek eignet sich dafür hervorragend." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 434c6aa3..b46a21d9 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -6030,9 +6030,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is -- Encryption secret: is configured UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1931141322"] = "Encryption secret: is configured" --- We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1943216839"] = "We use Rocket to implement the runtime API. This is necessary because the runtime must be able to communicate with the user interface (IPC). Rocket is a great framework for implementing web APIs in Rust." - -- Copies the following to the clipboard UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2029659664"] = "Copies the following to the clipboard" @@ -6135,6 +6132,12 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3178730036"] = "Have feature ide -- Hide Details UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3183837919"] = "Hide Details" +-- Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3208719461"] = "Axum server runs the internal axum service over a secure local connection. This helps AI Studio protect the communication between the Rust runtime and the user interface." + +-- Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3239817808"] = "Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running." + -- Update Pandoc UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3249965383"] = "Update Pandoc" @@ -6228,6 +6231,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T836298648"] = "Provided by confi -- We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T855925638"] = "We use this library to be able to read PowerPoint files. This allows us to insert content from slides into prompts and take PowerPoint files into account in RAG processes. We thank Nils Kruthoff for his work on this Rust crate." +-- Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T864851737"] = "Axum is used to provide the small internal service that connects the Rust runtime with the app's user interface. This lets both parts of AI Studio exchange information while the app is running." + -- For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T870640199"] = "For some data transfers, we need to encode the data in base64. This Rust library is great for this purpose." diff --git a/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs b/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs index e44dfa7f..4a066843 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.FileSystem.cs @@ -7,7 +7,8 @@ public sealed partial class RustService public async Task<DirectorySelectionResponse> SelectDirectory(string title, string? initialDirectory = null) { PreviousDirectory? previousDirectory = initialDirectory is null ? null : new (initialDirectory); - var result = await this.http.PostAsJsonAsync($"/select/directory?title={title}", previousDirectory, this.jsonRustSerializerOptions); + var encodedTitle = Uri.EscapeDataString(title); + var result = await this.http.PostAsJsonAsync($"/select/directory?title={encodedTitle}", previousDirectory, this.jsonRustSerializerOptions); if (!result.IsSuccessStatusCode) { this.logger!.LogError($"Failed to select a directory: '{result.StatusCode}'"); diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs b/app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs index 6d63f022..4a3f59d5 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs @@ -13,7 +13,16 @@ public sealed partial class RustService var response = await this.http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); if (!response.IsSuccessStatusCode) + { + var responseBody = await response.Content.ReadAsStringAsync(); + this.logger?.LogError( + "Failed to read arbitrary file data from Rust runtime. Status: {StatusCode}, reason: '{ReasonPhrase}', path: '{Path}', body: '{Body}'", + response.StatusCode, + response.ReasonPhrase, + path, + responseBody); return string.Empty; + } var resultBuilder = new StringBuilder(); diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index 6d03ec12..5f07c21c 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -99,6 +99,15 @@ dependencies = [ "x11rb", ] +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + [[package]] name = "asn1-rs" version = "0.7.1" @@ -323,21 +332,6 @@ dependencies = [ "debug_unsafe", ] -[[package]] -name = "atomic" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59bdb34bc650a32731b31bd8f0829cc15d24a708ee31559e0bb34f2bc320cba" - -[[package]] -name = "atomic" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" -dependencies = [ - "bytemuck", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -372,6 +366,80 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-server" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc" +dependencies = [ + "arc-swap", + "bytes", + "either", + "fs-err", + "http", + "http-body", + "hyper", + "hyper-util", + "pin-project-lite", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "base64" version = "0.21.7" @@ -384,12 +452,6 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "binascii" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "383d29d513d8764dcdc42ea295d979eb99c3c9f00607b3692cf68a431f7dca72" - [[package]] name = "bit-set" version = "0.8.0" @@ -812,7 +874,6 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" dependencies = [ - "percent-encoding", "time", "version_check", ] @@ -1144,39 +1205,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "devise" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1d90b0c4c777a2cad215e3c7be59ac7c15adf45cf76317009b7d096d46f651d" -dependencies = [ - "devise_codegen", - "devise_core", -] - -[[package]] -name = "devise_codegen" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71b28680d8be17a570a2334922518be6adc3f58ecc880cbb404eaeb8624fd867" -dependencies = [ - "devise_core", - "quote", -] - -[[package]] -name = "devise_core" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b035a542cf7abf01f2e3c4d5a7acbaebfefe120ae4efc7bde3df98186e4b8af7" -dependencies = [ - "bitflags 2.6.0", - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn 2.0.117", -] - [[package]] name = "digest" version = "0.10.7" @@ -1483,20 +1511,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "figment" -version = "0.10.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" -dependencies = [ - "atomic 0.6.0", - "pear", - "serde", - "toml 0.8.2", - "uncased", - "version_check", -] - [[package]] name = "file-format" version = "0.29.0" @@ -1614,6 +1628,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +dependencies = [ + "autocfg", + "tokio", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -1820,19 +1844,6 @@ dependencies = [ "x11", ] -[[package]] -name = "generator" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" -dependencies = [ - "cc", - "libc", - "log", - "rustversion", - "windows 0.48.0", -] - [[package]] name = "generic-array" version = "0.14.7" @@ -2072,35 +2083,16 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.26" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.14.0", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.1.0", + "http", "indexmap 2.14.0", "slab", "tokio", @@ -2157,12 +2149,6 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" - [[package]] name = "hermit-abi" version = "0.5.2" @@ -2194,17 +2180,6 @@ dependencies = [ "markup5ever", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.1.0" @@ -2216,17 +2191,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -2234,7 +2198,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.1.0", + "http", ] [[package]] @@ -2245,8 +2209,8 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.1.0", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -2264,43 +2228,21 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.30" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ + "atomic-waker", "bytes", "futures-channel", "futures-core", - "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", + "h2", + "http", + "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "h2 0.4.5", - "http 1.1.0", - "http-body 1.0.1", - "httparse", - "itoa", - "pin-project-lite", "smallvec", "tokio", "want", @@ -2313,13 +2255,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", - "http 1.1.0", - "hyper 1.6.0", + "http", + "hyper", "hyper-util", - "rustls 0.23.28", + "rustls", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.1", + "tokio-rustls", "tower-service", ] @@ -2331,7 +2273,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.6.0", + "hyper", "hyper-util", "native-tls", "tokio", @@ -2341,28 +2283,27 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.14" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", "bytes", "futures-channel", - "futures-core", "futures-util", - "http 1.1.0", - "http-body 1.0.1", - "hyper 1.6.0", + "http", + "http-body", + "hyper", "ipnet", "libc", "percent-encoding", "pin-project-lite", - "socket2 0.5.10", + "socket2", "system-configuration", "tokio", "tower-service", "tracing", - "windows-registry 0.5.3", + "windows-registry", ] [[package]] @@ -2614,12 +2555,6 @@ dependencies = [ "cfb", ] -[[package]] -name = "inlinable_string" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" - [[package]] name = "inout" version = "0.1.3" @@ -2655,17 +2590,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "is-terminal" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" -dependencies = [ - "hermit-abi 0.4.0", - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "is-wsl" version = "0.4.0" @@ -2887,7 +2811,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] @@ -2949,21 +2873,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "loom" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "serde", - "serde_json", - "tracing", - "tracing-subscriber", -] - [[package]] name = "lru-slab" version = "0.1.2" @@ -3003,13 +2912,10 @@ dependencies = [ ] [[package]] -name = "matchers" -version = "0.2.0" +name = "matchit" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "maybe-owned" @@ -3045,6 +2951,8 @@ dependencies = [ "aes", "arboard", "async-stream", + "axum", + "axum-server", "base64 0.22.1", "bytes", "calamine", @@ -3064,7 +2972,7 @@ dependencies = [ "rand_chacha 0.10.0", "rcgen", "reqwest", - "rocket", + "rustls", "serde", "serde_json", "sha2", @@ -3080,10 +2988,9 @@ dependencies = [ "tauri-plugin-updater", "tauri-plugin-window-state", "tempfile", - "time", "tokio", "tokio-stream", - "windows-registry 0.6.1", + "windows-registry", ] [[package]] @@ -3151,25 +3058,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "multer" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83e87776546dc87511aa5ee218730c92b666d7264ab6ed41f9d215af9cd5224b" -dependencies = [ - "bytes", - "encoding_rs", - "futures-util", - "http 1.1.0", - "httparse", - "memchr", - "mime", - "spin", - "tokio", - "tokio-util", - "version_check", -] - [[package]] name = "native-tls" version = "0.2.12" @@ -3324,16 +3212,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi 0.3.9", - "libc", -] - [[package]] name = "num_enum" version = "0.7.6" @@ -3837,29 +3715,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "pear" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" -dependencies = [ - "inlinable_string", - "pear_codegen", - "yansi", -] - -[[package]] -name = "pear_codegen" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" -dependencies = [ - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn 2.0.117", -] - [[package]] name = "pem" version = "3.0.4" @@ -4115,19 +3970,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "version_check", - "yansi", -] - [[package]] name = "qoi" version = "0.4.1" @@ -4168,8 +4010,8 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.28", - "socket2 0.6.2", + "rustls", + "socket2", "thiserror 2.0.12", "tokio", "tracing", @@ -4189,7 +4031,7 @@ dependencies = [ "rand 0.9.1", "ring", "rustc-hash", - "rustls 0.23.28", + "rustls", "rustls-pki-types", "slab", "thiserror 2.0.12", @@ -4207,7 +4049,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.2", + "socket2", "tracing", "windows-sys 0.60.2", ] @@ -4383,26 +4225,6 @@ dependencies = [ "thiserror 2.0.12", ] -[[package]] -name = "ref-cast" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf0a6f84d5f1d581da8b41b47ec8600871962f2a528115b542b362d4b744931" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc303e793d3734489387d205e9b186fac9c6cfacedd98cbb2e8a5943595f3e6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "regex" version = "1.10.5" @@ -4443,11 +4265,11 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2 0.4.5", - "http 1.1.0", - "http-body 1.0.1", + "h2", + "http", + "http-body", "http-body-util", - "hyper 1.6.0", + "hyper", "hyper-rustls", "hyper-tls", "hyper-util", @@ -4458,7 +4280,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.28", + "rustls", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -4466,7 +4288,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", - "tokio-rustls 0.26.1", + "tokio-rustls", "tokio-util", "tower", "tower-http", @@ -4516,91 +4338,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rocket" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a516907296a31df7dc04310e7043b61d71954d703b603cc6867a026d7e72d73f" -dependencies = [ - "async-stream", - "async-trait", - "atomic 0.5.3", - "binascii", - "bytes", - "either", - "figment", - "futures", - "indexmap 2.14.0", - "log", - "memchr", - "multer", - "num_cpus", - "parking_lot", - "pin-project-lite", - "rand 0.8.5", - "ref-cast", - "rocket_codegen", - "rocket_http", - "serde", - "serde_json", - "state", - "tempfile", - "time", - "tokio", - "tokio-stream", - "tokio-util", - "ubyte", - "version_check", - "yansi", -] - -[[package]] -name = "rocket_codegen" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575d32d7ec1a9770108c879fc7c47815a80073f96ca07ff9525a94fcede1dd46" -dependencies = [ - "devise", - "glob", - "indexmap 2.14.0", - "proc-macro2", - "quote", - "rocket_http", - "syn 2.0.117", - "unicode-xid", - "version_check", -] - -[[package]] -name = "rocket_http" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e274915a20ee3065f611c044bd63c40757396b6dbc057d6046aec27f14f882b9" -dependencies = [ - "cookie", - "either", - "futures", - "http 0.2.12", - "hyper 0.14.30", - "indexmap 2.14.0", - "log", - "memchr", - "pear", - "percent-encoding", - "pin-project-lite", - "ref-cast", - "rustls 0.21.12", - "rustls-pemfile", - "serde", - "smallvec", - "stable-pattern", - "state", - "time", - "tokio", - "tokio-rustls 0.24.1", - "uncased", -] - [[package]] name = "roxmltree" version = "0.20.0" @@ -4657,18 +4394,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - [[package]] name = "rustls" version = "0.23.28" @@ -4679,7 +4404,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.10", + "rustls-webpki", "subtle", "zeroize", ] @@ -4696,15 +4421,6 @@ dependencies = [ "security-framework 3.5.1", ] -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - [[package]] name = "rustls-pki-types" version = "1.14.0" @@ -4726,10 +4442,10 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.28", + "rustls", "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.103.10", + "rustls-webpki", "security-framework 3.5.1", "security-framework-sys", "webpki-root-certs", @@ -4744,19 +4460,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.101.7" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "rustls-webpki" -version = "0.103.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -4770,6 +4476,12 @@ version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +[[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" @@ -4815,28 +4527,12 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "security-framework" version = "2.11.1" @@ -4967,6 +4663,17 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_repr" version = "0.1.19" @@ -4996,6 +4703,18 @@ 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.9.0" @@ -5079,15 +4798,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "shared_child" version = "1.0.0" @@ -5140,16 +4850,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.2" @@ -5208,36 +4908,12 @@ dependencies = [ "system-deps", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "stable-pattern" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4564168c00635f88eaed410d5efa8131afa8d8699a612c80c455a0ba05c21045" -dependencies = [ - "memchr", -] - [[package]] name = "stable_deref_trait" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "state" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b8c4a4445d81357df8b1a650d0d0d6fbbbfe99d064aa5e02f3e4022061476d8" -dependencies = [ - "loom", -] - [[package]] name = "string_cache" version = "0.9.0" @@ -5364,9 +5040,9 @@ dependencies = [ [[package]] name = "system-configuration" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" dependencies = [ "bitflags 2.6.0", "core-foundation 0.9.4", @@ -5480,7 +5156,7 @@ dependencies = [ "glob", "gtk", "heck 0.5.0", - "http 1.1.0", + "http", "jni", "libc", "log", @@ -5703,14 +5379,14 @@ dependencies = [ "dirs", "flate2", "futures-util", - "http 1.1.0", + "http", "infer", "log", "minisign-verify", "osakit", "percent-encoding", "reqwest", - "rustls 0.23.28", + "rustls", "semver", "serde", "serde_json", @@ -5750,7 +5426,7 @@ dependencies = [ "cookie", "dpi", "gtk", - "http 1.1.0", + "http", "jni", "objc2 0.6.4", "objc2-ui-kit", @@ -5773,7 +5449,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3989df2ae1c476404fe0a2e8ffc4cfbde97e51efd613c2bb5355fbc9ab52cf0" dependencies = [ "gtk", - "http 1.1.0", + "http", "jni", "log", "objc2 0.6.4", @@ -5805,7 +5481,7 @@ dependencies = [ "dom_query", "dunce", "glob", - "http 1.1.0", + "http", "infer", "json-patch", "log", @@ -5904,16 +5580,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "thread_local" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" -dependencies = [ - "cfg-if", - "once_cell", -] - [[package]] name = "tiff" version = "0.9.1" @@ -5992,7 +5658,7 @@ dependencies = [ "mio", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.2", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -6018,23 +5684,13 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls 0.23.28", + "rustls", "tokio", ] @@ -6195,6 +5851,7 @@ dependencies = [ "tokio", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -6206,8 +5863,8 @@ dependencies = [ "bitflags 2.6.0", "bytes", "futures-util", - "http 1.1.0", - "http-body 1.0.1", + "http", + "http-body", "iri-string", "pin-project-lite", "tower", @@ -6233,6 +5890,7 @@ version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -6256,36 +5914,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", ] [[package]] @@ -6334,15 +5962,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "ubyte" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f720def6ce1ee2fc44d40ac9ed6d3a59c361c80a75a7aa8e75bb9baed31cf2ea" -dependencies = [ - "serde", -] - [[package]] name = "uds_windows" version = "1.2.1" @@ -6354,16 +5973,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "uncased" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" -dependencies = [ - "serde", - "version_check", -] - [[package]] name = "unic-char-property" version = "0.9.0" @@ -6491,12 +6100,6 @@ dependencies = [ "serde", ] -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - [[package]] name = "vcpkg" version = "0.2.15" @@ -6871,15 +6474,6 @@ dependencies = [ "windows-version", ] -[[package]] -name = "windows" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows" version = "0.61.3" @@ -7034,17 +6628,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-registry" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" -dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - [[package]] name = "windows-registry" version = "0.6.1" @@ -7566,7 +7149,7 @@ dependencies = [ "dunce", "gdkx11", "gtk", - "http 1.1.0", + "http", "javascriptcore-rs", "jni", "libc", @@ -7676,15 +7259,6 @@ dependencies = [ "lzma-sys", ] -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" -dependencies = [ - "is-terminal", -] - [[package]] name = "yasna" version = "0.5.2" diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 97328e92..df26409f 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -25,7 +25,9 @@ async-stream = "0.3.6" flexi_logger = "0.31.8" log = { version = "0.4.29", features = ["kv"] } once_cell = "1.21.4" -rocket = { version = "0.5.1", features = ["json", "tls"] } +axum = { version = "0.8.9", features = ["http2", "json", "query", "tokio"] } +axum-server = { version = "0.8.0", features = ["tls-rustls"] } +rustls = { version = "0.23.28", default-features = false, features = ["aws_lc_rs"] } rand = "0.10.1" rand_chacha = "0.10.0" base64 = "0.22.1" @@ -46,7 +48,6 @@ strum_macros = "0.28.0" sysinfo = "0.38.4" # Fixes security vulnerability downstream, where the upstream is not fixed yet: -time = "0.3.47" # -> Rocket bytes = "1.11.1" # -> almost every dependency [target.'cfg(target_os = "linux")'.dependencies] diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index d53a2f6d..1abd7951 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -1,13 +1,16 @@ use std::collections::HashMap; +use std::convert::Infallible; use std::sync::Mutex; use std::time::Duration; +use async_stream::stream; +use axum::body::Body; +use axum::http::header::CONTENT_TYPE; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use bytes::Bytes; use log::{debug, error, info, trace, warn}; use once_cell::sync::Lazy; -use rocket::{get, post}; -use rocket::response::stream::TextStream; -use rocket::serde::json::Json; -use rocket::serde::Serialize; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use strum_macros::Display; use tauri::{DragDropEvent,RunEvent, Manager, WindowEvent, generate_context}; use tauri::path::PathResolver; @@ -256,8 +259,7 @@ fn should_open_in_system_browser<R: tauri::Runtime>(webview: &tauri::Webview<R>, /// When the client disconnects, the stream is closed. But we try to not lose events in between. /// The client is expected to reconnect automatically when the connection is closed and continue /// listening for events. -#[get("/events")] -pub async fn get_event_stream(_token: APIToken) -> TextStream![String] { +pub async fn get_event_stream(_token: APIToken) -> Response { // Get the lock to the event broadcast sender: let event_broadcast_lock = EVENT_BROADCAST.lock().unwrap(); @@ -269,8 +271,7 @@ pub async fn get_event_stream(_token: APIToken) -> TextStream![String] { // Drop the lock to allow other access to the sender: drop(event_broadcast_lock); - // Create the event stream: - TextStream! { + let stream = stream! { loop { // Wait at most 3 seconds for an event: match time::timeout(Duration::from_secs(3), event_receiver.recv()).await { @@ -281,11 +282,11 @@ pub async fn get_event_stream(_token: APIToken) -> TextStream![String] { // is serialized as a single line so that the client can parse it // correctly: let event_json = serde_json::to_string(&event).unwrap(); - yield event_json; + yield Ok::<Bytes, Infallible>(Bytes::from(event_json)); // The client expects a newline after each event because we are using // a method to read the stream line-by-line: - yield "\n".to_string(); + yield Ok::<Bytes, Infallible>(Bytes::from("\n")); }, // Case: we lagged behind and missed some events @@ -305,15 +306,17 @@ pub async fn get_event_stream(_token: APIToken) -> TextStream![String] { // Again, we have to serialize the event as a single line: let event_json = serde_json::to_string(&ping_event).unwrap(); - yield event_json; + yield Ok::<Bytes, Infallible>(Bytes::from(event_json)); // The client expects a newline after each event because we are using // a method to read the stream line-by-line: - yield "\n".to_string(); + yield Ok::<Bytes, Infallible>(Bytes::from("\n")); }, } } - } + }; + + ([(CONTENT_TYPE, "application/jsonl")], Body::from_stream(stream)).into_response() } /// Data structure representing a Tauri event for our event API. @@ -428,7 +431,6 @@ pub async fn change_location_to(url: &str) { } /// Checks for updates. -#[get("/updates/check")] pub async fn check_for_update(_token: APIToken) -> Json<CheckUpdateResponse> { if is_dev() { warn!(Source = "Updater"; "The app is running in development mode; skipping update check."); @@ -514,7 +516,6 @@ pub struct CheckUpdateResponse { } /// Installs the update. -#[get("/updates/install")] pub async fn install_update(_token: APIToken) { if is_dev() { warn!(Source = "Updater"; "The app is running in development mode; skipping update installation."); @@ -623,8 +624,7 @@ fn register_shortcut_with_callback<R: tauri::Runtime>( } /// Requests a controlled shutdown of the entire desktop application. -#[post("/app/exit")] -pub fn exit_app(_token: APIToken) -> Json<AppExitResponse> { +pub async fn exit_app(_token: APIToken) -> Json<AppExitResponse> { let app_handle = { let main_window_lock = MAIN_WINDOW.lock().unwrap(); match main_window_lock.as_ref() { @@ -653,8 +653,7 @@ pub fn exit_app(_token: APIToken) -> Json<AppExitResponse> { /// Registers or updates a global shortcut. If the shortcut string is empty, /// the existing shortcut for that name will be unregistered. -#[post("/shortcuts/register", data = "<payload>")] -pub fn register_shortcut(_token: APIToken, payload: Json<RegisterShortcutRequest>) -> Json<ShortcutResponse> { +pub async fn register_shortcut(_token: APIToken, payload: Json<RegisterShortcutRequest>) -> Json<ShortcutResponse> { let id = payload.id; let new_shortcut = payload.shortcut.clone(); @@ -761,8 +760,7 @@ pub struct ShortcutValidationResponse { /// Validates a shortcut string without registering it. /// Checks if the shortcut syntax is valid and if it /// conflicts with existing shortcuts. -#[post("/shortcuts/validate", data = "<payload>")] -pub fn validate_shortcut(_token: APIToken, payload: Json<ValidateShortcutRequest>) -> Json<ShortcutValidationResponse> { +pub async fn validate_shortcut(_token: APIToken, payload: Json<ValidateShortcutRequest>) -> Json<ShortcutValidationResponse> { let shortcut = payload.shortcut.clone(); // Empty shortcuts are always valid (means "disabled"): @@ -816,8 +814,7 @@ pub fn validate_shortcut(_token: APIToken, payload: Json<ValidateShortcutRequest /// The shortcuts remain in our internal map, so they can be re-registered on resume. /// This is useful when opening a dialog to configure shortcuts, so the user can /// press the current shortcut to re-enter it without triggering the action. -#[post("/shortcuts/suspend")] -pub fn suspend_shortcuts(_token: APIToken) -> Json<ShortcutResponse> { +pub async fn suspend_shortcuts(_token: APIToken) -> Json<ShortcutResponse> { // Get the main window to access the global shortcut manager: let main_window_lock = MAIN_WINDOW.lock().unwrap(); let main_window = match main_window_lock.as_ref() { @@ -853,8 +850,7 @@ pub fn suspend_shortcuts(_token: APIToken) -> Json<ShortcutResponse> { } /// Resumes shortcut processing by re-registering all shortcuts with the OS. -#[post("/shortcuts/resume")] -pub fn resume_shortcuts(_token: APIToken) -> Json<ShortcutResponse> { +pub async fn resume_shortcuts(_token: APIToken) -> Json<ShortcutResponse> { // Get the main window to access the global shortcut manager: let main_window_lock = MAIN_WINDOW.lock().unwrap(); let main_window = match main_window_lock.as_ref() { @@ -954,36 +950,6 @@ fn validate_shortcut_syntax(shortcut: &str) -> bool { has_key } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn tauri_localhost_is_tauri_asset_url() { - let https_url = tauri::Url::parse("https://tauri.localhost/index.html").unwrap(); - let http_url = tauri::Url::parse("http://tauri.localhost/index.html").unwrap(); - - assert!(is_tauri_asset_url(&https_url)); - assert!(is_tauri_asset_url(&http_url)); - } - - #[test] - fn localhost_app_url_is_not_tauri_asset_url() { - let url = tauri::Url::parse("http://localhost:12345/").unwrap(); - - assert!(!is_tauri_asset_url(&url)); - assert!(is_local_http_url(&url)); - } - - #[test] - fn external_url_is_not_internal_url() { - let url = tauri::Url::parse("https://example.com/").unwrap(); - - assert!(!is_tauri_asset_url(&url)); - assert!(!is_local_http_url(&url)); - } -} - fn set_pdfium_path<R: tauri::Runtime>(path_resolver: &PathResolver<R>) { let resource_dir = match path_resolver.resource_dir() { Ok(path) => path, @@ -1012,3 +978,33 @@ fn set_pdfium_path<R: tauri::Runtime>(path_resolver: &PathResolver<R>) { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tauri_localhost_is_tauri_asset_url() { + let https_url = tauri::Url::parse("https://tauri.localhost/index.html").unwrap(); + let http_url = tauri::Url::parse("http://tauri.localhost/index.html").unwrap(); + + assert!(is_tauri_asset_url(&https_url)); + assert!(is_tauri_asset_url(&http_url)); + } + + #[test] + fn localhost_app_url_is_not_tauri_asset_url() { + let url = tauri::Url::parse("http://localhost:12345/").unwrap(); + + assert!(!is_tauri_asset_url(&url)); + assert!(is_local_http_url(&url)); + } + + #[test] + fn external_url_is_not_internal_url() { + let url = tauri::Url::parse("https://example.com/").unwrap(); + + assert!(!is_tauri_asset_url(&url)); + assert!(!is_local_http_url(&url)); + } +} \ No newline at end of file diff --git a/runtime/src/clipboard.rs b/runtime/src/clipboard.rs index b00617f2..bdb612ff 100644 --- a/runtime/src/clipboard.rs +++ b/runtime/src/clipboard.rs @@ -1,14 +1,13 @@ use arboard::Clipboard; use log::{debug, error}; -use rocket::post; -use rocket::serde::json::Json; +use axum::Json; use serde::Serialize; use crate::api_token::APIToken; use crate::encryption::{EncryptedText, ENCRYPTION}; /// Sets the clipboard text to the provided encrypted text. -#[post("/clipboard/set", data = "<encrypted_text>")] -pub fn set_clipboard(_token: APIToken, encrypted_text: EncryptedText) -> Json<SetClipboardResponse> { +pub async fn set_clipboard(_token: APIToken, encrypted_text: String) -> Json<SetClipboardResponse> { + let encrypted_text = EncryptedText::new(encrypted_text); // Decrypt this text first: let decrypted_text = match ENCRYPTION.decrypt(&encrypted_text) { diff --git a/runtime/src/dotnet.rs b/runtime/src/dotnet.rs index 7cca4599..c5158e13 100644 --- a/runtime/src/dotnet.rs +++ b/runtime/src/dotnet.rs @@ -5,7 +5,6 @@ use base64::Engine; use base64::prelude::BASE64_STANDARD; use log::{error, info, warn}; use once_cell::sync::Lazy; -use rocket::get; use tauri::Url; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_shell::ShellExt; @@ -89,8 +88,7 @@ fn sanitize_stdout_line(line: &str) -> String { /// Returns the desired port of the .NET server. Our .NET app calls this endpoint to get /// the port where the .NET server should listen to. -#[get("/system/dotnet/port")] -pub fn dotnet_port(_token: APIToken) -> String { +pub async fn dotnet_port(_token: APIToken) -> String { let dotnet_server_port = *DOTNET_SERVER_PORT; format!("{dotnet_server_port}") } @@ -179,7 +177,6 @@ pub fn start_dotnet_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>) { } /// This endpoint is called by the .NET server to signal that the server is ready. -#[get("/system/dotnet/ready")] pub async fn dotnet_ready(_token: APIToken) { // We create a manual scope for the lock to be released as soon as possible. diff --git a/runtime/src/encryption.rs b/runtime/src/encryption.rs index 41506855..2c7828b3 100644 --- a/runtime/src/encryption.rs +++ b/runtime/src/encryption.rs @@ -9,19 +9,13 @@ use once_cell::sync::Lazy; use pbkdf2::pbkdf2; use rand::rngs::SysRng; use rand::{Rng, SeedableRng}; -use rocket::{data, Data, Request}; -use rocket::data::ToByteUnit; -use rocket::http::Status; -use rocket::serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize}; use sha2::Sha512; -use tokio::io::AsyncReadExt; type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>; type Aes256CbcDec = cbc::Decryptor<aes::Aes256>; -type DataOutcome<'r, T> = data::Outcome<'r, T>; - /// The encryption instance used for the IPC channel. pub static ENCRYPTION: Lazy<Encryption> = Lazy::new(|| { // @@ -170,27 +164,4 @@ impl fmt::Display for EncryptedText { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "**********") } -} - -/// Use Case: When we receive encrypted text from the client as body (e.g., in a POST request). -/// We must interpret the body as EncryptedText. -#[rocket::async_trait] -impl<'r> data::FromData<'r> for EncryptedText { - type Error = String; - - /// Parses the data as EncryptedText. - async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> DataOutcome<'r, Self> { - let content_type = req.content_type(); - if content_type.map_or(true, |ct| !ct.is_text()) { - return DataOutcome::Forward((data, Status::Ok)); - } - - let mut stream = data.open(2.mebibytes()); - let mut body = String::new(); - if let Err(e) = stream.read_to_string(&mut body).await { - return DataOutcome::Error((Status::InternalServerError, format!("Failed to read data: {}", e))); - } - - DataOutcome::Success(EncryptedText(body)) - } } \ No newline at end of file diff --git a/runtime/src/environment.rs b/runtime/src/environment.rs index 593ac2d7..68198fbd 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -1,7 +1,6 @@ use crate::api_token::APIToken; +use axum::Json; use log::{debug, info, warn}; -use rocket::get; -use rocket::serde::json::Json; use serde::Serialize; use std::collections::{HashMap, HashSet}; use std::env; @@ -29,8 +28,7 @@ pub static CONFIG_DIRECTORY: OnceLock<String> = OnceLock::new(); static USER_LANGUAGE: OnceLock<String> = OnceLock::new(); /// Returns the config directory. -#[get("/system/directories/config")] -pub fn get_config_directory(_token: APIToken) -> String { +pub async fn get_config_directory(_token: APIToken) -> String { match CONFIG_DIRECTORY.get() { Some(config_directory) => config_directory.clone(), None => String::from(""), @@ -38,8 +36,7 @@ pub fn get_config_directory(_token: APIToken) -> String { } /// Returns the data directory. -#[get("/system/directories/data")] -pub fn get_data_directory(_token: APIToken) -> String { +pub async fn get_data_directory(_token: APIToken) -> String { match DATA_DIRECTORY.get() { Some(data_directory) => data_directory.clone(), None => String::from(""), @@ -150,8 +147,7 @@ fn detect_user_language() -> (String, LanguageDetectionSource) { ) } -#[get("/system/language")] -pub fn read_user_language(_token: APIToken) -> String { +pub async fn read_user_language(_token: APIToken) -> String { USER_LANGUAGE .get_or_init(|| { let (user_language, source) = detect_user_language(); @@ -194,8 +190,7 @@ struct EnterpriseSourceData { encryption_secret: String, } -#[get("/system/enterprise/config/id")] -pub fn read_enterprise_env_config_id(_token: APIToken) -> String { +pub async fn read_enterprise_env_config_id(_token: APIToken) -> String { debug!("Trying to read the effective enterprise configuration ID."); resolve_effective_enterprise_config_source() .configs @@ -205,8 +200,7 @@ pub fn read_enterprise_env_config_id(_token: APIToken) -> String { .unwrap_or_default() } -#[get("/system/enterprise/config/server")] -pub fn read_enterprise_env_config_server_url(_token: APIToken) -> String { +pub async fn read_enterprise_env_config_server_url(_token: APIToken) -> String { debug!("Trying to read the effective enterprise configuration server URL."); resolve_effective_enterprise_config_source() .configs @@ -216,15 +210,13 @@ pub fn read_enterprise_env_config_server_url(_token: APIToken) -> String { .unwrap_or_default() } -#[get("/system/enterprise/config/encryption_secret")] -pub fn read_enterprise_env_config_encryption_secret(_token: APIToken) -> String { +pub async fn read_enterprise_env_config_encryption_secret(_token: APIToken) -> String { debug!("Trying to read the effective enterprise configuration encryption secret."); resolve_effective_enterprise_secret_source().encryption_secret } /// Returns all enterprise configurations from the effective source. -#[get("/system/enterprise/configs")] -pub fn read_enterprise_configs(_token: APIToken) -> Json<Vec<EnterpriseConfig>> { +pub async fn read_enterprise_configs(_token: APIToken) -> Json<Vec<EnterpriseConfig>> { info!("Trying to read the effective enterprise configurations."); Json(resolve_effective_enterprise_config_source().configs) } diff --git a/runtime/src/file_actions.rs b/runtime/src/file_actions.rs index 94eeb629..3ef7d81d 100644 --- a/runtime/src/file_actions.rs +++ b/runtime/src/file_actions.rs @@ -1,7 +1,7 @@ use log::{error, info}; -use rocket::post; -use rocket::serde::{Deserialize, Serialize}; -use rocket::serde::json::Json; +use axum::extract::Query; +use axum::Json; +use serde::{Deserialize, Serialize}; use tauri_plugin_dialog::{DialogExt, FileDialogBuilder}; use crate::api_token::APIToken; use crate::app_window::MAIN_WINDOW; @@ -11,6 +11,11 @@ pub struct PreviousDirectory { path: String, } +#[derive(Deserialize)] +pub struct SelectDirectoryQuery { + title: String, +} + #[derive(Clone, Deserialize)] pub struct FileTypeFilter { filter_name: String, @@ -61,10 +66,9 @@ pub struct PreviousFile { } /// Let the user select a directory. -#[post("/select/directory?<title>", data = "<previous_directory>")] -pub fn select_directory( +pub async fn select_directory( _token: APIToken, - title: &str, + Query(query): Query<SelectDirectoryQuery>, previous_directory: Option<Json<PreviousDirectory>>, ) -> Json<DirectorySelectionResponse> { let main_window_lock = MAIN_WINDOW.lock().unwrap(); @@ -79,7 +83,7 @@ pub fn select_directory( } }; - let mut dialog = main_window.dialog().file().set_parent(main_window).set_title(title); + let mut dialog = main_window.dialog().file().set_parent(main_window).set_title(&query.title); if let Some(previous) = previous_directory { dialog = dialog.set_directory(previous.path.clone()); } @@ -118,8 +122,7 @@ pub fn select_directory( } /// Let the user select a file. -#[post("/select/file", data = "<payload>")] -pub fn select_file( +pub async fn select_file( _token: APIToken, payload: Json<SelectFileOptions>, ) -> Json<FileSelectionResponse> { @@ -178,8 +181,7 @@ pub fn select_file( } /// Let the user select some files. -#[post("/select/files", data = "<payload>")] -pub fn select_files( +pub async fn select_files( _token: APIToken, payload: Json<SelectFileOptions>, ) -> Json<FilesSelectionResponse> { @@ -229,8 +231,7 @@ pub fn select_files( } } -#[post("/save/file", data = "<payload>")] -pub fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json<FileSaveResponse> { +pub async fn save_file(_token: APIToken, payload: Json<SaveFileOptions>) -> Json<FileSaveResponse> { // Create a new file dialog builder: let file_dialog = MAIN_WINDOW .lock() diff --git a/runtime/src/file_data.rs b/runtime/src/file_data.rs index b0ba1b24..43446f46 100644 --- a/runtime/src/file_data.rs +++ b/runtime/src/file_data.rs @@ -1,22 +1,24 @@ use std::cmp::min; +use std::convert::Infallible; use crate::api_token::APIToken; use crate::pandoc::PandocProcessBuilder; use crate::pdfium::PdfiumInit; use async_stream::stream; +use axum::extract::Query; +use axum::extract::rejection::QueryRejection; +use axum::response::sse::{Event, Sse}; use base64::{engine::general_purpose, Engine as _}; use calamine::{open_workbook_auto, Reader}; use file_format::{FileFormat, Kind}; use futures::{Stream, StreamExt}; use pdfium_render::prelude::Pdfium; use pptx_to_md::{ImageHandlingMode, ParserConfig, PptxContainer}; -use rocket::get; -use rocket::response::stream::{Event, EventStream}; -use rocket::serde::Serialize; -use rocket::tokio::select; -use rocket::Shutdown; +use serde::{Deserialize, Deserializer, Serialize}; +use serde::de::{Error as SerdeError, Visitor}; use std::path::Path; use std::pin::Pin; -use log::{debug, error}; +use std::fmt; +use log::{debug, error, warn}; use tokio::io::AsyncBufReadExt; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; @@ -82,39 +84,95 @@ const IMAGE_SEGMENT_SIZE_IN_CHARS: usize = 8_192; // equivalent to ~ 5500 token type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>; type ChunkStream = Pin<Box<dyn Stream<Item = Result<Chunk>> + Send>>; -#[get("/retrieval/fs/extract?<path>&<stream_id>&<extract_images>")] -pub async fn extract_data(_token: APIToken, path: String, stream_id: String, extract_images: bool, mut end: Shutdown) -> EventStream![] { - EventStream! { - let stream_result = stream_data(&path, extract_images).await; - let id_ref = &stream_id; - - match stream_result { - Ok(mut stream) => { - loop { - let chunk = select! { - chunk = stream.next() => match chunk { - Some(Ok(mut chunk)) => { - chunk.set_stream_id(id_ref); - chunk - }, - Some(Err(e)) => { - yield Event::json(&format!("Error: {e}")); - break; - }, - None => break, - }, - _ = &mut end => break, - }; - - yield Event::json(&chunk); - } - }, +#[derive(Deserialize)] +pub struct ExtractDataQuery { + path: String, + stream_id: String, + #[serde(deserialize_with = "deserialize_bool_case_insensitive")] + extract_images: bool, +} - Err(e) => { - yield Event::json(&format!("Error starting stream: {e}")); +fn deserialize_bool_case_insensitive<'de, D>(deserializer: D) -> std::result::Result<bool, D::Error> +where + D: Deserializer<'de>, +{ + struct BoolVisitor; + + impl<'de> Visitor<'de> for BoolVisitor { + type Value = bool; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("a boolean value") + } + + fn visit_bool<E>(self, value: bool) -> std::result::Result<Self::Value, E> { + Ok(value) + } + + fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E> + where + E: SerdeError, + { + match value.to_ascii_lowercase().as_str() { + "true" | "1" => Ok(true), + "false" | "0" => Ok(false), + _ => Err(E::invalid_value(serde::de::Unexpected::Str(value), &self)), } } } + + deserializer.deserialize_any(BoolVisitor) +} + +pub async fn extract_data( + _token: APIToken, + query: std::result::Result<Query<ExtractDataQuery>, QueryRejection>, +) -> Sse<impl Stream<Item = std::result::Result<Event, Infallible>>> { + let query = match query { + Ok(Query(query)) => Ok(query), + Err(e) => { + let message = format!("Invalid query for '/retrieval/fs/extract': {e}"); + warn!("{message}"); + Err(message) + }, + }; + + let stream = stream! { + match query { + Ok(query) => { + let stream_result = stream_data(&query.path, query.extract_images).await; + let id_ref = &query.stream_id; + + match stream_result { + Ok(mut stream) => { + while let Some(chunk) = stream.next().await { + match chunk { + Ok(mut chunk) => { + chunk.set_stream_id(id_ref); + yield Ok(Event::default().json_data(&chunk).unwrap_or_else(|e| Event::default().data(format!("Error: {e}")))); + }, + + Err(e) => { + yield Ok(Event::default().json_data(format!("Error: {e}")).unwrap_or_else(|_| Event::default().data(format!("Error: {e}")))); + break; + }, + } + } + }, + + Err(e) => { + yield Ok(Event::default().json_data(format!("Error starting stream: {e}")).unwrap_or_else(|_| Event::default().data(format!("Error starting stream: {e}")))); + } + }; + }, + + Err(e) => { + yield Ok(Event::default().json_data(format!("Error starting stream: {e}")).unwrap_or_else(|_| Event::default().data(format!("Error starting stream: {e}")))); + }, + } + }; + + Sse::new(stream) } async fn stream_data(file_path: &str, extract_images: bool) -> Result<ChunkStream> { diff --git a/runtime/src/log.rs b/runtime/src/log.rs index a38d942c..18f0921a 100644 --- a/runtime/src/log.rs +++ b/runtime/src/log.rs @@ -8,9 +8,8 @@ use flexi_logger::{DeferredNow, Duplicate, FileSpec, Logger, LoggerHandle}; use flexi_logger::writers::FileLogWriter; use log::{kv, Level}; use log::kv::{Key, Value, VisitSource}; -use rocket::{get, post}; -use rocket::serde::json::Json; -use rocket::serde::{Deserialize, Serialize}; +use axum::Json; +use serde::{Deserialize, Serialize}; use crate::api_token::APIToken; use crate::environment::is_dev; @@ -34,14 +33,17 @@ pub fn init_logging() { false => log_config.push_str("info, "), }; - // Set the log level for the Rocket library: - log_config.push_str("rocket=info, "); - - // Set the log level for the Rocket server: - log_config.push_str("rocket::server=warn, "); - - // Set the log level for the Reqwest library: - log_config.push_str("reqwest::async_impl::client=info"); + // Keep noisy HTTP/TLS internals at info level even in development builds: + log_config.push_str("h2=info, "); + log_config.push_str("hyper=info, "); + log_config.push_str("hyper_util=info, "); + log_config.push_str("axum=info, "); + log_config.push_str("axum_server=info, "); + log_config.push_str("tower=info, "); + log_config.push_str("tower_http=info, "); + log_config.push_str("rustls=info, "); + log_config.push_str("tokio_rustls=info, "); + log_config.push_str("reqwest=info"); // Configure the initial filename. On Unix systems, the file should start // with a dot to be hidden. @@ -224,7 +226,6 @@ fn file_logger_format( write!(w, "{}", &record.args()) } -#[get("/log/paths")] pub async fn get_log_paths(_token: APIToken) -> Json<LogPathsResponse> { Json(LogPathsResponse { log_startup_path: LOG_STARTUP_PATH.get().expect("No startup log path was set").clone(), @@ -269,9 +270,7 @@ fn log_with_level( } /// Logs an event from the .NET server. -#[post("/log/event", data = "<event>")] -pub fn log_event(_token: APIToken, event: Json<LogEvent>) -> Json<LogEventResponse> { - let event = event.into_inner(); +pub async fn log_event(_token: APIToken, Json(event): Json<LogEvent>) -> Json<LogEventResponse> { let level = parse_dotnet_log_level(&event.level); let message = event.message.as_str(); let category = event.category.as_str(); diff --git a/runtime/src/main.rs b/runtime/src/main.rs index 00a7ba90..84d280fe 100644 --- a/runtime/src/main.rs +++ b/runtime/src/main.rs @@ -1,7 +1,6 @@ // Prevents an additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -extern crate rocket; extern crate core; use log::{info, warn}; @@ -12,7 +11,6 @@ use mindwork_ai_studio::log::init_logging; use mindwork_ai_studio::metadata::MetaData; use mindwork_ai_studio::runtime_api::start_runtime_api; - #[tokio::main] async fn main() { let metadata = MetaData::init_from_string(include_str!("../../metadata.txt")); diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index 11e52005..c24b7d6d 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -7,9 +7,8 @@ use std::path::Path; use std::sync::{Arc, Mutex, OnceLock}; use log::{debug, error, info, warn}; use once_cell::sync::Lazy; -use rocket::get; -use rocket::serde::json::Json; -use rocket::serde::Serialize; +use axum::Json; +use serde::Serialize; use crate::api_token::{APIToken}; use crate::environment::{is_dev, DATA_DIRECTORY}; use crate::certificate_factory::generate_certificate; @@ -70,8 +69,7 @@ pub struct ProvideQdrantInfo { unavailable_reason: Option<String>, } -#[get("/system/qdrant/info")] -pub fn qdrant_port(_token: APIToken) -> Json<ProvideQdrantInfo> { +pub async fn qdrant_port(_token: APIToken) -> Json<ProvideQdrantInfo> { let status = QDRANT_STATUS.lock().unwrap(); let is_available = status.is_available; let unavailable_reason = status.unavailable_reason.clone(); diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index 4d881fe3..213c8a55 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -1,12 +1,16 @@ use log::info; use once_cell::sync::Lazy; -use rocket::config::Shutdown; -use rocket::figment::Figment; -use rocket::routes; +use axum::routing::{get, post}; +use axum::Router; +use axum_server::tls_rustls::RustlsConfig; +use std::net::SocketAddr; +use std::sync::Once; use crate::runtime_certificate::{CERTIFICATE, CERTIFICATE_PRIVATE_KEY}; use crate::environment::is_dev; use crate::network::get_available_port; +static RUSTLS_CRYPTO_PROVIDER_INIT: Once = Once::new(); + /// The port used for the runtime API server. In the development environment, we use a fixed /// port, in the production environment we use the next available port. This differentiation /// is necessary because we cannot communicate the port to the .NET server in the development @@ -24,109 +28,55 @@ pub static API_SERVER_PORT: Lazy<u16> = Lazy::new(|| { pub fn start_runtime_api() { let api_port = *API_SERVER_PORT; info!("Try to start the API server on 'http://localhost:{api_port}'..."); - - // Get the shutdown configuration: - let shutdown = create_shutdown(); - // Configure the runtime API server: - let figment = Figment::from(rocket::Config::release_default()) + let app = Router::new() + .route("/system/dotnet/port", get(crate::dotnet::dotnet_port)) + .route("/system/dotnet/ready", get(crate::dotnet::dotnet_ready)) + .route("/system/qdrant/info", get(crate::qdrant::qdrant_port)) + .route("/clipboard/set", post(crate::clipboard::set_clipboard)) + .route("/events", get(crate::app_window::get_event_stream)) + .route("/updates/check", get(crate::app_window::check_for_update)) + .route("/updates/install", get(crate::app_window::install_update)) + .route("/app/exit", post(crate::app_window::exit_app)) + .route("/select/directory", post(crate::file_actions::select_directory)) + .route("/select/file", post(crate::file_actions::select_file)) + .route("/select/files", post(crate::file_actions::select_files)) + .route("/save/file", post(crate::file_actions::save_file)) + .route("/secrets/get", post(crate::secret::get_secret)) + .route("/secrets/store", post(crate::secret::store_secret)) + .route("/secrets/delete", post(crate::secret::delete_secret)) + .route("/system/directories/config", get(crate::environment::get_config_directory)) + .route("/system/directories/data", get(crate::environment::get_data_directory)) + .route("/system/language", get(crate::environment::read_user_language)) + .route("/system/enterprise/config/id", get(crate::environment::read_enterprise_env_config_id)) + .route("/system/enterprise/config/server", get(crate::environment::read_enterprise_env_config_server_url)) + .route("/system/enterprise/config/encryption_secret", get(crate::environment::read_enterprise_env_config_encryption_secret)) + .route("/system/enterprise/configs", get(crate::environment::read_enterprise_configs)) + .route("/retrieval/fs/extract", get(crate::file_data::extract_data)) + .route("/log/paths", get(crate::log::get_log_paths)) + .route("/log/event", post(crate::log::log_event)) + .route("/shortcuts/register", post(crate::app_window::register_shortcut)) + .route("/shortcuts/validate", post(crate::app_window::validate_shortcut)) + .route("/shortcuts/suspend", post(crate::app_window::suspend_shortcuts)) + .route("/shortcuts/resume", post(crate::app_window::resume_shortcuts)); - // We use the next available port which was determined before: - .merge(("port", api_port)) - - // The runtime API server should be accessible only from the local machine: - .merge(("address", "127.0.0.1")) - - // We do not want to use the Ctrl+C signal to stop the server: - .merge(("ctrlc", false)) - - // Set a name for the server: - .merge(("ident", "AI Studio Runtime API")) - - // Set the maximum number of workers and blocking threads: - .merge(("workers", 3)) - .merge(("max_blocking", 12)) - - // No colors and emojis in the log output: - .merge(("cli_colors", false)) - - // Read the TLS certificate and key from the generated certificate data in-memory: - .merge(("tls.certs", CERTIFICATE.get().unwrap())) - .merge(("tls.key", CERTIFICATE_PRIVATE_KEY.get().unwrap())) - - // Set the shutdown configuration: - .merge(("shutdown", shutdown)); - - // - // Start the runtime API server in a separate thread. This is necessary - // because the server is blocking, and we need to run the Tauri app in - // parallel: - // tauri::async_runtime::spawn(async move { - rocket::custom(figment) - .mount("/", routes![ - crate::dotnet::dotnet_port, - crate::dotnet::dotnet_ready, - crate::qdrant::qdrant_port, - crate::clipboard::set_clipboard, - crate::app_window::get_event_stream, - crate::app_window::check_for_update, - crate::app_window::install_update, - crate::app_window::exit_app, - crate::file_actions::select_directory, - crate::file_actions::select_file, - crate::file_actions::select_files, - crate::file_actions::save_file, - crate::secret::get_secret, - crate::secret::store_secret, - crate::secret::delete_secret, - crate::environment::get_data_directory, - crate::environment::get_config_directory, - crate::environment::read_user_language, - crate::environment::read_enterprise_env_config_id, - crate::environment::read_enterprise_env_config_server_url, - crate::environment::read_enterprise_env_config_encryption_secret, - crate::environment::read_enterprise_configs, - crate::file_data::extract_data, - crate::log::get_log_paths, - crate::log::log_event, - crate::app_window::register_shortcut, - crate::app_window::validate_shortcut, - crate::app_window::suspend_shortcuts, - crate::app_window::resume_shortcuts, - ]) - .ignite().await.unwrap() - .launch().await.unwrap(); + install_rustls_crypto_provider(); + + let cert = CERTIFICATE.get().unwrap().clone(); + let key = CERTIFICATE_PRIVATE_KEY.get().unwrap().clone(); + let tls_config = RustlsConfig::from_pem(cert, key).await.unwrap(); + let addr = SocketAddr::from(([127, 0, 0, 1], api_port)); + + axum_server::bind_rustls(addr, tls_config) + .serve(app.into_make_service()) + .await + .unwrap(); }); } -fn create_shutdown() -> Shutdown { - // - // Create a shutdown configuration, depending on the operating system: - // - #[cfg(unix)] - { - use std::collections::HashSet; - let mut shutdown = Shutdown { - // We do not want to use the Ctrl+C signal to stop the server: - ctrlc: false, - - // Everything else is set to default for now: - ..Shutdown::default() - }; - - shutdown.signals = HashSet::new(); - shutdown - } - - #[cfg(windows)] - { - Shutdown { - // We do not want to use the Ctrl+C signal to stop the server: - ctrlc: false, - - // Everything else is set to default for now: - ..Shutdown::default() - } - } +fn install_rustls_crypto_provider() { + RUSTLS_CRYPTO_PROVIDER_INIT.call_once(|| { + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + }); } \ No newline at end of file diff --git a/runtime/src/runtime_api_token.rs b/runtime/src/runtime_api_token.rs index f1e762c9..795f4936 100644 --- a/runtime/src/runtime_api_token.rs +++ b/runtime/src/runtime_api_token.rs @@ -1,33 +1,29 @@ use once_cell::sync::Lazy; -use rocket::http::Status; -use rocket::Request; -use rocket::request::FromRequest; +use axum::extract::FromRequestParts; +use axum::http::request::Parts; +use axum::http::StatusCode; use crate::api_token::{generate_api_token, APIToken}; -pub static API_TOKEN: Lazy<APIToken> = Lazy::new(|| generate_api_token()); +pub static API_TOKEN: Lazy<APIToken> = Lazy::new(generate_api_token); -/// The request outcome type used to handle API token requests. -type RequestOutcome<R, T> = rocket::request::Outcome<R, T>; +impl<S> FromRequestParts<S> for APIToken +where + S: Send + Sync, +{ + type Rejection = StatusCode; -/// The request outcome implementation for the API token. -#[rocket::async_trait] -impl<'r> FromRequest<'r> for APIToken { - type Error = APITokenError; - - /// Handles the API token requests. - async fn from_request(request: &'r Request<'_>) -> RequestOutcome<Self, Self::Error> { - let token = request.headers().get_one("token"); - match token { + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { + match parts.headers.get("token").and_then(|value| value.to_str().ok()) { Some(token) => { let received_token = APIToken::from_hex_text(token); if API_TOKEN.validate(&received_token) { - RequestOutcome::Success(received_token) + Ok(received_token) } else { - RequestOutcome::Error((Status::Unauthorized, APITokenError::Invalid)) + Err(StatusCode::UNAUTHORIZED) } } - None => RequestOutcome::Error((Status::Unauthorized, APITokenError::Missing)), + None => Err(StatusCode::UNAUTHORIZED), } } } diff --git a/runtime/src/secret.rs b/runtime/src/secret.rs index 5ae07c8b..2f074a62 100644 --- a/runtime/src/secret.rs +++ b/runtime/src/secret.rs @@ -1,15 +1,13 @@ use keyring::Entry; use log::{error, info, warn}; -use rocket::post; -use rocket::serde::json::Json; +use axum::Json; use serde::{Deserialize, Serialize}; use keyring::error::Error::NoEntry; use crate::api_token::APIToken; use crate::encryption::{EncryptedText, ENCRYPTION}; /// Stores a secret in the secret store using the operating system's keyring. -#[post("/secrets/store", data = "<request>")] -pub fn store_secret(_token: APIToken, request: Json<StoreSecret>) -> Json<StoreSecretResponse> { +pub async fn store_secret(_token: APIToken, request: Json<StoreSecret>) -> Json<StoreSecretResponse> { let user_name = request.user_name.as_str(); let decrypted_text = match ENCRYPTION.decrypt(&request.secret) { Ok(text) => text, @@ -60,8 +58,7 @@ pub struct StoreSecretResponse { } /// Retrieves a secret from the secret store using the operating system's keyring. -#[post("/secrets/get", data = "<request>")] -pub fn get_secret(_token: APIToken, request: Json<RequestSecret>) -> Json<RequestedSecret> { +pub async fn get_secret(_token: APIToken, request: Json<RequestSecret>) -> Json<RequestedSecret> { let user_name = request.user_name.as_str(); let service = format!("mindwork-ai-studio::{}", request.destination); let entry = Entry::new(service.as_str(), user_name).unwrap(); @@ -121,8 +118,7 @@ pub struct RequestedSecret { } /// Deletes a secret from the secret store using the operating system's keyring. -#[post("/secrets/delete", data = "<request>")] -pub fn delete_secret(_token: APIToken, request: Json<RequestSecret>) -> Json<DeleteSecretResponse> { +pub async fn delete_secret(_token: APIToken, request: Json<RequestSecret>) -> Json<DeleteSecretResponse> { let user_name = request.user_name.as_str(); let service = format!("mindwork-ai-studio::{}", request.destination); let entry = Entry::new(service.as_str(), user_name).unwrap(); From 0089849e0c3df60ddfc4b6aa0001ad7d3cde47d6 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Wed, 13 May 2026 11:58:16 +0200 Subject: [PATCH 09/21] Prepared release v26.5.3 (#758) --- app/MindWork AI Studio/Components/Changelog.Logs.cs | 1 + app/MindWork AI Studio/wwwroot/changelog/v26.5.3.md | 3 ++- app/MindWork AI Studio/wwwroot/changelog/v26.5.4.md | 1 + metadata.txt | 8 ++++---- runtime/Cargo.lock | 2 +- runtime/Cargo.toml | 2 +- runtime/tauri.conf.json | 4 ++-- 7 files changed, 12 insertions(+), 9 deletions(-) create mode 100644 app/MindWork AI Studio/wwwroot/changelog/v26.5.4.md diff --git a/app/MindWork AI Studio/Components/Changelog.Logs.cs b/app/MindWork AI Studio/Components/Changelog.Logs.cs index 714a61e8..c8d74949 100644 --- a/app/MindWork AI Studio/Components/Changelog.Logs.cs +++ b/app/MindWork AI Studio/Components/Changelog.Logs.cs @@ -13,6 +13,7 @@ public partial class Changelog public static readonly Log[] LOGS = [ + new (238, "v26.5.3, build 238 (2026-05-13 09:50 UTC)", "v26.5.3.md"), new (237, "v26.5.2, build 237 (2026-05-06 16:38 UTC)", "v26.5.2.md"), new (236, "v26.5.1, build 236 (2026-05-06 13:06 UTC)", "v26.5.1.md"), new (235, "v26.4.1, build 235 (2026-04-17 17:25 UTC)", "v26.4.1.md"), diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.3.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.3.md index ff2b7c29..37c3d83e 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.5.3.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.3.md @@ -1 +1,2 @@ -# v26.5.3, build 238 (2026-05-xx xx:xx UTC) +# v26.5.3, build 238 (2026-05-13 09:50 UTC) +- Migrated away from Rocket to Axum for our internal IPC API. Please do not install this prerelease manually. Production versions, such as v26.4.1, will ignore this update. We are using this prerelease to test the clean update path. After a successful test, this prerelease will be removed. \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.4.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.4.md new file mode 100644 index 00000000..18f23ffc --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.4.md @@ -0,0 +1 @@ +# v26.5.4, build 239 (2026-05-xx xx:xx UTC) diff --git a/metadata.txt b/metadata.txt index bea38b51..ded455da 100644 --- a/metadata.txt +++ b/metadata.txt @@ -1,12 +1,12 @@ -26.5.2 -2026-05-06 16:38:01 UTC -237 +26.5.3 +2026-05-13 09:50:18 UTC +238 9.0.116 (commit fb4af7e1b3) 9.0.15 (commit 4250c8399a) 1.95.0 (commit 59807616e) 8.15.0 2.11.1 -bcf15e91881, release +d69eab88072, release osx-arm64 144.0.7543.0 1.17.1 \ No newline at end of file diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index 5f07c21c..38a5e01b 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -2946,7 +2946,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mindwork-ai-studio" -version = "26.5.2" +version = "26.5.3" dependencies = [ "aes", "arboard", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index df26409f..7ec28118 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mindwork-ai-studio" -version = "26.5.2" +version = "26.5.3" edition = "2024" description = "MindWork AI Studio" authors = ["Thorsten Sommer"] diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index 88e11f70..824934a1 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -1,7 +1,7 @@ { "productName": "MindWork AI Studio", "mainBinaryName": "MindWork AI Studio", - "version": "26.5.2", + "version": "26.5.3", "identifier": "com.github.mindwork-ai.ai-studio", "build": { @@ -43,7 +43,7 @@ "installMode": "passive" }, "endpoints": [ - "https://github.com/MindWorkAI/AI-Studio/releases/download/v26.5.3/latest.json" + "https://github.com/MindWorkAI/AI-Studio/releases/download/v26.5.4/latest.json" ], "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDM3MzE4MTM4RTNDMkM0NEQKUldSTnhNTGpPSUV4TjFkczFxRFJOZWgydzFQN1dmaFlKbXhJS1YyR1RKS1RnR09jYUpMaGsrWXYK" } From 3360c2fa2944b01be2748559a6423e17a29e9b6e Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Wed, 13 May 2026 14:03:36 +0200 Subject: [PATCH 10/21] Prepared test release v26.5.4 (#759) --- app/MindWork AI Studio/Components/Changelog.Logs.cs | 1 + app/MindWork AI Studio/wwwroot/changelog/v26.5.4.md | 3 ++- app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md | 1 + metadata.txt | 8 ++++---- runtime/Cargo.lock | 2 +- runtime/Cargo.toml | 2 +- runtime/tauri.conf.json | 2 +- 7 files changed, 11 insertions(+), 8 deletions(-) create mode 100644 app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md diff --git a/app/MindWork AI Studio/Components/Changelog.Logs.cs b/app/MindWork AI Studio/Components/Changelog.Logs.cs index c8d74949..a3585023 100644 --- a/app/MindWork AI Studio/Components/Changelog.Logs.cs +++ b/app/MindWork AI Studio/Components/Changelog.Logs.cs @@ -13,6 +13,7 @@ public partial class Changelog public static readonly Log[] LOGS = [ + new (239, "v26.5.4, build 239 (2026-05-13 11:58 UTC)", "v26.5.4.md"), new (238, "v26.5.3, build 238 (2026-05-13 09:50 UTC)", "v26.5.3.md"), new (237, "v26.5.2, build 237 (2026-05-06 16:38 UTC)", "v26.5.2.md"), new (236, "v26.5.1, build 236 (2026-05-06 13:06 UTC)", "v26.5.1.md"), diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.4.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.4.md index 18f23ffc..9e6c72ca 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.5.4.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.4.md @@ -1 +1,2 @@ -# v26.5.4, build 239 (2026-05-xx xx:xx UTC) +# v26.5.4, build 239 (2026-05-13 11:58 UTC) +- Migrated away from Rocket to Axum for our internal IPC API. Please do not install this prerelease manually. Production versions, such as v26.4.1, will ignore this update. We are using this prerelease to test the clean update path. After a successful test, this prerelease will be removed. \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md new file mode 100644 index 00000000..237ed260 --- /dev/null +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md @@ -0,0 +1 @@ +# v26.5.5, build 240 (2026-05-xx xx:xx UTC) diff --git a/metadata.txt b/metadata.txt index ded455da..8265e475 100644 --- a/metadata.txt +++ b/metadata.txt @@ -1,12 +1,12 @@ -26.5.3 -2026-05-13 09:50:18 UTC -238 +26.5.4 +2026-05-13 11:58:02 UTC +239 9.0.116 (commit fb4af7e1b3) 9.0.15 (commit 4250c8399a) 1.95.0 (commit 59807616e) 8.15.0 2.11.1 -d69eab88072, release +0089849e0c3, release osx-arm64 144.0.7543.0 1.17.1 \ No newline at end of file diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index 38a5e01b..1d47465e 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -2946,7 +2946,7 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mindwork-ai-studio" -version = "26.5.3" +version = "26.5.4" dependencies = [ "aes", "arboard", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 7ec28118..c500df0c 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mindwork-ai-studio" -version = "26.5.3" +version = "26.5.4" edition = "2024" description = "MindWork AI Studio" authors = ["Thorsten Sommer"] diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json index 824934a1..69d26cfd 100644 --- a/runtime/tauri.conf.json +++ b/runtime/tauri.conf.json @@ -1,7 +1,7 @@ { "productName": "MindWork AI Studio", "mainBinaryName": "MindWork AI Studio", - "version": "26.5.3", + "version": "26.5.4", "identifier": "com.github.mindwork-ai.ai-studio", "build": { From 6fc69751b9598756ba59b6674117fe7fac8f0cc0 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Wed, 13 May 2026 22:18:14 +0200 Subject: [PATCH 11/21] Updated documentation & readme (#760) --- README.md | 5 ++--- .../wwwroot/changelog/v26.5.5.md | 2 ++ documentation/Build.md | 4 ++-- documentation/Setup.md | 2 +- documentation/Ubuntu DEB Install 1.png | Bin 37147 -> 0 bytes documentation/Ubuntu DEB Install 2.png | Bin 32321 -> 0 bytes documentation/Ubuntu DEB Open.png | Bin 31902 -> 0 bytes 7 files changed, 7 insertions(+), 6 deletions(-) delete mode 100644 documentation/Ubuntu DEB Install 1.png delete mode 100644 documentation/Ubuntu DEB Install 2.png delete mode 100644 documentation/Ubuntu DEB Open.png diff --git a/README.md b/README.md index 5b69c065..40f0302c 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,11 @@ Since November 2024: Work on RAG (integration of your data and files) has begun. - [x] ~~App: Implement an [ERI](https://github.com/MindWorkAI/ERI) server coding assistant (PR [#231](https://github.com/MindWorkAI/AI-Studio/pull/231))~~ - [x] ~~App: Management of data sources (local & external data via [ERI](https://github.com/MindWorkAI/ERI)) (PR [#259](https://github.com/MindWorkAI/AI-Studio/pull/259), [#273](https://github.com/MindWorkAI/AI-Studio/pull/273))~~ - [x] ~~Runtime: Extract data from txt / md / pdf / docx / xlsx files (PR [#374](https://github.com/MindWorkAI/AI-Studio/pull/374))~~ -- [ ] (*Optional*) Runtime: Implement internal embedding provider through [fastembed-rs](https://github.com/Anush008/fastembed-rs) - [x] ~~App: Implement dialog for checking & handling [pandoc](https://pandoc.org/) installation ([PR #393](https://github.com/MindWorkAI/AI-Studio/pull/393), [PR #487](https://github.com/MindWorkAI/AI-Studio/pull/487))~~ - [x] ~~App: Implement external embedding providers ([PR #654](https://github.com/MindWorkAI/AI-Studio/pull/654))~~ -- [ ] App: Implement the process to vectorize one local file using embeddings +- [ ] App: Implement the process to vectorize one local file using embeddings (PR [#756](https://github.com/MindWorkAI/AI-Studio/pull/756)) - [x] ~~Runtime: Integration of the vector database [Qdrant](https://github.com/qdrant/qdrant) ([PR #580](https://github.com/MindWorkAI/AI-Studio/pull/580))~~ -- [ ] App: Implement the continuous process of vectorizing data +- [ ] App: Implement the continuous process of vectorizing data (PR [#756](https://github.com/MindWorkAI/AI-Studio/pull/756)) - [x] ~~App: Define a common retrieval context interface for the integration of RAG processes in chats (PR [#281](https://github.com/MindWorkAI/AI-Studio/pull/281), [#284](https://github.com/MindWorkAI/AI-Studio/pull/284), [#286](https://github.com/MindWorkAI/AI-Studio/pull/286), [#287](https://github.com/MindWorkAI/AI-Studio/pull/287))~~ - [x] ~~App: Define a common augmentation interface for the integration of RAG processes in chats (PR [#288](https://github.com/MindWorkAI/AI-Studio/pull/288), [#289](https://github.com/MindWorkAI/AI-Studio/pull/289))~~ - [x] ~~App: Integrate data sources in chats (PR [#282](https://github.com/MindWorkAI/AI-Studio/pull/282))~~ diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md index 237ed260..36886ce9 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md @@ -1 +1,3 @@ # v26.5.5, build 240 (2026-05-xx xx:xx UTC) +- Improved the app's security foundation with major modernization of the native runtime and its internal communication layer. This work is mostly invisible during everyday use, but it replaces older components that no longer received the security updates we require. We also continued updating security-sensitive dependencies so AI Studio stays on a healthier, better maintained base. +- Upgraded Tauri from v1.8.3 to v2.11.1. \ No newline at end of file diff --git a/documentation/Build.md b/documentation/Build.md index 600999fe..8022cd7d 100644 --- a/documentation/Build.md +++ b/documentation/Build.md @@ -9,7 +9,7 @@ Therefore, we cannot provide a static list here that is valid for all Linux syst ## Prerequisites 1. Install the [.NET 9 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/9.0). 2. [Install the Rust compiler](https://www.rust-lang.org/tools/install) in the latest stable version. -3. Met the prerequisites for building [Tauri](https://tauri.app/v1/guides/getting-started/prerequisites/). Node.js is **not** required, though. +3. Meet the prerequisites for building [Tauri](https://v2.tauri.app/start/prerequisites/). Node.js is **not** required, though. 4. The core team uses [JetBrains](https://www.jetbrains.com/) [Rider](https://www.jetbrains.com/rider/) and [RustRover](https://www.jetbrains.com/rust/) for development. Both IDEs are free to use for open-source projects for non-commercial use. They are available for macOS, Linux, and Windows systems. Profiles are provided for these IDEs, so you can get started right away. However, you can also use a different IDE. 4. Clone the repository. @@ -17,7 +17,7 @@ Therefore, we cannot provide a static list here that is valid for all Linux syst Regardless of whether you want to build the app locally for yourself (not trusting the pre-built binaries) or test your changes before creating a PR, you have to run the following commands at least once: 1. Open a terminal. -2. Install the Tauri CLI by running `cargo install --version 1.6.2 tauri-cli`. +2. Install the Tauri CLI by running `cargo install tauri-cli --version 2.11.0 --locked`. 3. Navigate to the `/app/Build` directory within the repository. 4. Run `dotnet run build` to build the entire app. diff --git a/documentation/Setup.md b/documentation/Setup.md index c6e4bfd8..6b545627 100644 --- a/documentation/Setup.md +++ b/documentation/Setup.md @@ -84,4 +84,4 @@ We have to figure out if you have an Intel/AMD or a modern ARM system on your Li 2. Open a terminal and navigate to the Downloads folder: `cd Downloads`. 3. Make the AppImage executable: `chmod +x mind-work-ai-studio_amd64.AppImage`. 4. You might want to move the AppImage to a more convenient location, e.g., your home directory: `mv mind-work-ai-studio_amd64.AppImage ~/`. -4. Now you can run the AppImage from your file manager (double-click) or the terminal: `./mind-work-ai-studio_amd64.AppImage`. \ No newline at end of file +5. Now you can run the AppImage from your file manager (double-click) or the terminal: `./mind-work-ai-studio_amd64.AppImage`. \ No newline at end of file diff --git a/documentation/Ubuntu DEB Install 1.png b/documentation/Ubuntu DEB Install 1.png deleted file mode 100644 index bb09ae75f623f662c71a2d3861344a552c0bcde4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37147 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 diff --git a/documentation/Ubuntu DEB Install 2.png b/documentation/Ubuntu DEB Install 2.png deleted file mode 100644 index bd5ff07ab51497112014985546b6514c9ce8571f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32321 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�(~QMRx4Wbr0=Oz4NJ@L~htn<K?(sJrZ3NY|R? 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 diff --git a/documentation/Ubuntu DEB Open.png b/documentation/Ubuntu DEB Open.png deleted file mode 100644 index f53ab5bd33dfe0bed0714e5c6abb8e3b0213b401..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31902 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�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 From d46688f36456abdb5bb0412423540e663663e930 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Thu, 14 May 2026 17:16:28 +0200 Subject: [PATCH 12/21] Upgraded dependencies (#761) --- .github/workflows/build-and-release.yml | 28 +- .../Assistants/I18N/allTexts.lua | 6 +- .../MindWork AI Studio.csproj | 6 +- .../Pages/Information.razor | 2 +- .../plugin.lua | 6 +- .../plugin.lua | 6 +- app/MindWork AI Studio/packages.lock.json | 50 +- .../wwwroot/changelog/v26.5.5.md | 8 +- metadata.txt | 8 +- runtime/Cargo.lock | 850 ++++++++---------- runtime/Cargo.toml | 37 +- runtime/src/encryption.rs | 6 +- runtime/src/main.rs | 2 + runtime/src/secret.rs | 78 +- 14 files changed, 534 insertions(+), 559 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 3bcbc09f..2fe8cc6d 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -771,17 +771,29 @@ jobs: PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }} run: | bundles="${{ matrix.tauri_bundle }}" + tauri_config_args=() if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" = "true" ]; then echo "Running PR test build without updater bundle signing" bundles="${{ matrix.tauri_bundle_pr }}" + tauri_config_args=(--config '{"bundle":{"createUpdaterArtifacts":false}}') else export TAURI_SIGNING_PRIVATE_KEY="$PRIVATE_PUBLISH_KEY" export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$PRIVATE_PUBLISH_KEY_PASSWORD" fi cd runtime - cargo tauri build --target ${{ matrix.rust_target }} --bundles "$bundles" + cargo tauri build --target ${{ matrix.rust_target }} --bundles "$bundles" "${tauri_config_args[@]}" + + if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" = "true" ]; then + updater_artifact_count=$(find target/${{ matrix.rust_target }}/release/bundle -type f \( -name '*.app.tar.gz*' -o -name '*.AppImage.tar.gz*' -o -name '*nsis.zip*' \) | wc -l) + + if [ "$updater_artifact_count" -ne 0 ]; then + echo "PR builds must not generate updater artifacts." + find target/${{ matrix.rust_target }}/release/bundle -type f \( -name '*.app.tar.gz*' -o -name '*.AppImage.tar.gz*' -o -name '*nsis.zip*' \) + exit 1 + fi + fi if [ "${{ needs.determine_run_mode.outputs.is_pr_build }}" != "true" ] && [[ "${{ matrix.platform }}" == macos* ]]; then app_update_archive_count=$(find target/${{ matrix.rust_target }}/release/bundle/macos -maxdepth 1 -name '*.app.tar.gz' | wc -l) @@ -800,17 +812,29 @@ jobs: PRIVATE_PUBLISH_KEY_PASSWORD: ${{ secrets.PRIVATE_PUBLISH_KEY_PASSWORD }} run: | $bundles = "${{ matrix.tauri_bundle }}" + $tauriConfigArgs = @() if ("${{ needs.determine_run_mode.outputs.is_pr_build }}" -eq "true") { Write-Output "Running PR test build without updater bundle signing" $bundles = "${{ matrix.tauri_bundle_pr }}" + $tauriConfigArgs = @("--config", '{"bundle":{"createUpdaterArtifacts":false}}') } else { $env:TAURI_SIGNING_PRIVATE_KEY="$env:PRIVATE_PUBLISH_KEY" $env:TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$env:PRIVATE_PUBLISH_KEY_PASSWORD" } cd runtime - cargo tauri build --target ${{ matrix.rust_target }} --bundles $bundles + cargo tauri build --target ${{ matrix.rust_target }} --bundles $bundles @tauriConfigArgs + + if ("${{ needs.determine_run_mode.outputs.is_pr_build }}" -eq "true") { + $updaterArtifacts = Get-ChildItem -Path "target/${{ matrix.rust_target }}/release/bundle" -Recurse -File -Include "*.app.tar.gz*", "*.AppImage.tar.gz*", "*nsis.zip*" -ErrorAction SilentlyContinue + + if ($updaterArtifacts.Count -ne 0) { + Write-Error "PR builds must not generate updater artifacts." + $updaterArtifacts | ForEach-Object { Write-Error $_.FullName } + exit 1 + } + } - name: Upload artifact (macOS) if: startsWith(matrix.platform, 'macos') diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 2c340b17..314d30c2 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -6019,9 +6019,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1890416390"] = "Check for update -- Vision UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1892426825"] = "Vision" --- In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1915240766"] = "In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library." - -- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant." @@ -6160,6 +6157,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio runs w -- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri! UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!" +-- AI Studio stores secrets like API keys in your operating system’s secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3527399572"] = "AI Studio stores secrets like API keys in your operating system’s secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service." + -- Motivation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation" diff --git a/app/MindWork AI Studio/MindWork AI Studio.csproj b/app/MindWork AI Studio/MindWork AI Studio.csproj index e214e7e6..7cebafb9 100644 --- a/app/MindWork AI Studio/MindWork AI Studio.csproj +++ b/app/MindWork AI Studio/MindWork AI Studio.csproj @@ -50,12 +50,12 @@ <ItemGroup> <PackageReference Include="CodeBeam.MudBlazor.Extensions" Version="8.3.0" /> <PackageReference Include="HtmlAgilityPack" Version="1.12.4" /> - <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.15" /> + <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.16" /> <PackageReference Include="MudBlazor" Version="8.15.0" /> <PackageReference Include="MudBlazor.Markdown" Version="8.11.0" /> - <PackageReference Include="Qdrant.Client" Version="1.17.0" /> + <PackageReference Include="Qdrant.Client" Version="1.18.1" /> <PackageReference Include="ReverseMarkdown" Version="5.0.0" /> - <PackageReference Include="LuaCSharp" Version="0.5.3" /> + <PackageReference Include="LuaCSharp" Version="0.5.5" /> </ItemGroup> <ItemGroup> diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index 244e8f3e..119611cb 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -284,7 +284,7 @@ <ThirdPartyComponent Name="Rustls" Developer="Joe Birr-Pixton, Dirkjan Ochtman, Daniel McCarney, Brian Smith, Jacob Hoffman-Andrews, Jorge Aparicio & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rustls/rustls/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/rustls/rustls" UseCase="@T("Rustls helps secure the internal connection between the app's user interface and the Rust runtime. This protects the local communication that AI Studio needs while it is running.")"/> <ThirdPartyComponent Name="serde" Developer="Erick Tryzelaar, David Tolnay & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/serde-rs/serde/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/serde-rs/serde" UseCase="@T("Now we have multiple systems, some developed in .NET and others in Rust. The data format JSON is responsible for translating data between both worlds (called data serialization and deserialization). Serde takes on this task in the Rust world. The counterpart in the .NET world is an integral part of .NET and is located in System.Text.Json.")"/> <ThirdPartyComponent Name="strum_macros" Developer="Peter Glotfelty & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/Peternator7/strum/blob/master/LICENSE" RepositoryUrl="https://github.com/Peternator7/strum" UseCase="@T("This crate provides derive macros for Rust enums, which we use to reduce boilerplate when implementing string conversions and metadata for runtime types. This is helpful for the communication between our Rust and .NET systems.")"/> - <ThirdPartyComponent Name="keyring" Developer="Walther Chen, Daniel Brotsky & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/hwchen/keyring-rs/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/hwchen/keyring-rs" UseCase="@T("In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library.")"/> + <ThirdPartyComponent Name="keyring-core" Developer="Daniel Brotsky & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/open-source-cooperative/keyring-core/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/open-source-cooperative/keyring-core" UseCase="@T("AI Studio stores secrets like API keys in your operating system’s secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service.")"/> <ThirdPartyComponent Name="arboard" Developer="Artur Kovacs, Avi Weinstock, 1Password & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/arboard/blob/master/LICENSE-MIT.txt" RepositoryUrl="https://github.com/1Password/arboard" UseCase="@T("To be able to use the responses of the LLM in other apps, we often use the clipboard of the respective operating system. Unfortunately, in .NET there is no solution that works with all operating systems. Therefore, I have opted for this library in Rust. This way, data transfer to other apps works on every system.")"/> <ThirdPartyComponent Name="tokio" Developer="Alex Crichton, Carl Lerche, Alice Ryhl, Taiki Endo, Ivan Petkov, Eliza Weisman, Lucio Franco & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/tokio-rs/tokio/blob/master/LICENSE" RepositoryUrl="https://github.com/tokio-rs/tokio" UseCase="@T("Code in the Rust language can be specified as synchronous or asynchronous. Unlike .NET and the C# language, Rust cannot execute asynchronous code by itself. Rust requires support in the form of an executor for this. Tokio is one such executor.")"/> <ThirdPartyComponent Name="futures" Developer="Alex Crichton, Taiki Endo, Taylor Cramer, Nemo157, Josef Brandl, Aaron Turon & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/rust-lang/futures-rs/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/rust-lang/futures-rs" UseCase="@T("This is a library providing the foundations for asynchronous programming in Rust. It includes key trait definitions like Stream, as well as utilities like join!, select!, and various futures combinator methods which enable expressive asynchronous control flow.")"/> diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index b74ec6a3..fb4216bd 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -6021,9 +6021,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1890416390"] = "Nach Updates suc -- Vision UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1892426825"] = "Vision" --- In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1915240766"] = "Um ein beliebiges LLM nutzen zu können, muss jeder User seinen sogenannten API-Schlüssel für jeden LLM-Anbieter speichern. Dieser Schlüssel muss sicher aufbewahrt werden – ähnlich wie ein Passwort. Die sicherste Methode hierfür bieten Betriebssysteme wie macOS, Windows und Linux: Sie verfügen über Mechanismen, solche Daten – sofern vorhanden – auf spezieller Sicherheits-Hardware zu speichern. Da dies derzeit in .NET nicht möglich ist, verwenden wir diese Rust-Bibliothek." - -- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "Diese Bibliothek wird verwendet, um HTML in Markdown umzuwandeln. Das ist zum Beispiel notwendig, wenn Sie eine URL als Eingabe für einen Assistenten angeben." @@ -6162,6 +6159,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio wird m -- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri! UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri wird verwendet, um die Blazor-Benutzeroberfläche bereitzustellen. Es ist ein großartiges Projekt, das die Erstellung von Desktop-Anwendungen mit Webtechnologien ermöglicht. Ich liebe Tauri!" +-- AI Studio stores secrets like API keys in your operating system’s secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3527399572"] = "AI Studio speichert vertrauliche Daten wie API-Schlüssel im sicheren Speicher Ihres Betriebssystems. Die Bibliothek keyring-core übernimmt dies, indem sie eine Verbindung zum macOS-Schlüsselbund, zur Windows-Anmeldeinformationsverwaltung und zum Linux Secret Service herstellt." + -- Motivation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index b46a21d9..26be03ee 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -6021,9 +6021,6 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1890416390"] = "Check for update -- Vision UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1892426825"] = "Vision" --- In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library. -UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1915240766"] = "In order to use any LLM, each user must store their so-called API key for each LLM provider. This key must be kept secure, similar to a password. The safest way to do this is offered by operating systems like macOS, Windows, and Linux: They have mechanisms to store such data, if available, on special security hardware. Since this is currently not possible in .NET, we use this Rust library." - -- This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T1924365263"] = "This library is used to convert HTML to Markdown. This is necessary, e.g., when you provide a URL as input for an assistant." @@ -6162,6 +6159,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3449345633"] = "AI Studio runs w -- Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri! UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3494984593"] = "Tauri is used to host the Blazor user interface. It is a great project that allows the creation of desktop applications using web technologies. I love Tauri!" +-- AI Studio stores secrets like API keys in your operating system’s secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3527399572"] = "AI Studio stores secrets like API keys in your operating system’s secure credential store. The keyring-core library handles this by connecting to macOS Keychain, Windows Credential Manager, and Linux Secret Service." + -- Motivation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3563271893"] = "Motivation" diff --git a/app/MindWork AI Studio/packages.lock.json b/app/MindWork AI Studio/packages.lock.json index 0ca69dc7..311fe569 100644 --- a/app/MindWork AI Studio/packages.lock.json +++ b/app/MindWork AI Studio/packages.lock.json @@ -22,24 +22,28 @@ }, "LuaCSharp": { "type": "Direct", - "requested": "[0.5.3, )", - "resolved": "0.5.3", - "contentHash": "qpgmCaNx08+eiWOmz7U/mXOH8DXUyLW8fsCukKjN8hVled2y4HrapsZlmrnIf9iaNfEQusUR/8d1M2XX6NIzbQ==" + "requested": "[0.5.5, )", + "resolved": "0.5.5", + "contentHash": "IL44DCbMtEafyiy8DzHFd/f+1pXuDUVFJMCJPAu8vQHNfO3ADSoWSOKMg9Py1za/ZE1K0gs0jll1viInoN+19Q==", + "dependencies": { + "LuaCSharp.Annotations": "0.5.5", + "LuaCSharp.SourceGenerator": "0.5.5" + } }, "Microsoft.Extensions.FileProviders.Embedded": { "type": "Direct", - "requested": "[9.0.15, )", - "resolved": "9.0.15", - "contentHash": "XFlI3ZISL344QdPLtaXG0yPyjkHQR82DYXrJa9aF00Qeu7dDnFxwFgP/ItkkyiLjAe/NSj6vksxOdnelXGT1vQ==", + "requested": "[9.0.16, )", + "resolved": "9.0.16", + "contentHash": "QRlSWz7zEplBxETrySKK3qpPm/7NPaRGnUpEXQNP3k6Ht2KdVy59JcoUPXlNGnNE3tJd3ycXfMeWqxBG6SyV0w==", "dependencies": { - "Microsoft.Extensions.FileProviders.Abstractions": "9.0.15" + "Microsoft.Extensions.FileProviders.Abstractions": "9.0.16" } }, "Microsoft.NET.ILLink.Tasks": { "type": "Direct", - "requested": "[9.0.15, )", - "resolved": "9.0.15", - "contentHash": "EejcbfCMR77Dthy77qxRbEShmzLApHZUPqXMBVQK+A0pNrRThkaHoGGMGvbq/gTkC/waKcDEgjBkbaejB58Wtw==" + "requested": "[9.0.16, )", + "resolved": "9.0.16", + "contentHash": "ccPBYGLPJt8DeJTUzQ0JzOh/iuUAgnjayU63PokVywAhUOx+dzDKSPTL7AG94U/VpvNXflTT2AjsFAIF1+bXBw==" }, "MudBlazor": { "type": "Direct", @@ -64,9 +68,9 @@ }, "Qdrant.Client": { "type": "Direct", - "requested": "[1.17.0, )", - "resolved": "1.17.0", - "contentHash": "QFNtVu4Kiz6NHAAi2UQk+Ia64/qyX1NMecQGIBGnKqFOlpnxI3OCCBRBKXWGPk/c+4vAmR3Dj+cQ9apqX0zU8A==", + "requested": "[1.18.1, )", + "resolved": "1.18.1", + "contentHash": "eBwFLihGMvN02/jr/BNdcop2XmtA10y8VMOclVZ7K2H8yheAhl7jbkf7I8e4X3RYpT+cAxgcalP4xmOhgs4KJg==", "dependencies": { "Google.Protobuf": "3.31.0", "Grpc.Net.Client": "2.71.0" @@ -113,6 +117,16 @@ "Grpc.Core.Api": "2.71.0" } }, + "LuaCSharp.Annotations": { + "type": "Transitive", + "resolved": "0.5.5", + "contentHash": "5VcwcTNGCY5YXLz2BRko5/Z0YGd6MZqNsnnfPOsGHHpAtqWPFbD0vtOZR4jUqaQLtQUvl2+WRfmIOhp6L2S0rw==" + }, + "LuaCSharp.SourceGenerator": { + "type": "Transitive", + "resolved": "0.5.5", + "contentHash": "2xHKGc1bYXTsmSzZCNmKkuAU6A+1azulNiPY/ICKBSHIgEPMNRQ7JS6PvAClrHe6bk8SKcC/fbba6igtDzDaAw==" + }, "Markdig": { "type": "Transitive", "resolved": "0.41.3", @@ -182,10 +196,10 @@ }, "Microsoft.Extensions.FileProviders.Abstractions": { "type": "Transitive", - "resolved": "9.0.15", - "contentHash": "yzWilnNU/MvHINapPhY6iFAeApZnhToXbEBplORucn01hFc1F6ZaKt0V9dHYpUMun8WR9cSnq1ky35FWREVZbA==", + "resolved": "9.0.16", + "contentHash": "/YLSWDs+p0Y4+UGPoWI3uUNq7R5/f/8zw8XeViuhfSTGnPowoqbllBE9aR4TteFgNfIH4IHkhUwSlhMLB0aL8g==", "dependencies": { - "Microsoft.Extensions.Primitives": "9.0.15" + "Microsoft.Extensions.Primitives": "9.0.16" } }, "Microsoft.Extensions.Localization": { @@ -223,8 +237,8 @@ }, "Microsoft.Extensions.Primitives": { "type": "Transitive", - "resolved": "9.0.15", - "contentHash": "WRPJ9kpIwsOcghRT0tduIqiz7CDv7WsnL4kTJavtHS4j5AW++4LlR63oOSTL2o/zLR4T1z0/FQMgrnsPJ5bpQQ==" + "resolved": "9.0.16", + "contentHash": "w5RE1MR0lnAElsRJaFd2POIXl/H62aBKmfX8ibYmRmbk0JB9V/9jR0VD5NxiP1ETWpnDAnPguTSe7fF/FdsHEQ==" }, "Microsoft.JSInterop": { "type": "Transitive", diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md index 36886ce9..af2ad840 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md @@ -1,3 +1,9 @@ # v26.5.5, build 240 (2026-05-xx xx:xx UTC) - Improved the app's security foundation with major modernization of the native runtime and its internal communication layer. This work is mostly invisible during everyday use, but it replaces older components that no longer received the security updates we require. We also continued updating security-sensitive dependencies so AI Studio stays on a healthier, better maintained base. -- Upgraded Tauri from v1.8.3 to v2.11.1. \ No newline at end of file +- Upgraded the native secret storage integration to `keyring-core`, keeping API keys in the secure credential store provided by the operating system. +- Upgraded Rust to v1.95.0. +- Upgraded .NET to v9.0.16. +- Upgraded Tauri to v2.11.1. +- Upgraded PDFium to v148.0.7763.0. +- Upgraded Qdrant to v1.18.0. +- Upgraded other dependencies as well. \ No newline at end of file diff --git a/metadata.txt b/metadata.txt index 8265e475..f348ab3c 100644 --- a/metadata.txt +++ b/metadata.txt @@ -1,12 +1,12 @@ 26.5.4 2026-05-13 11:58:02 UTC 239 -9.0.116 (commit fb4af7e1b3) -9.0.15 (commit 4250c8399a) +9.0.117 (commit 6e241a69c1) +9.0.16 (commit a1e6809fb8) 1.95.0 (commit 59807616e) 8.15.0 2.11.1 0089849e0c3, release osx-arm64 -144.0.7543.0 -1.17.1 \ No newline at end of file +148.0.7763.0 +1.18.0 \ No newline at end of file diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index 1d47465e..c8894cac 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -21,10 +21,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher", + "cipher 0.4.4", "cpufeatures 0.2.12", ] +[[package]] +name = "aes" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" +dependencies = [ + "cipher 0.5.1", + "cpubits", + "cpufeatures 0.3.0", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -70,6 +81,17 @@ version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +[[package]] +name = "apple-native-keyring-store" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7be2f067ccd8d4b4d4a66ddafe0f32a5dff31732f32dbff85fefc40929b1f72" +dependencies = [ + "keyring-core", + "log", + "security-framework", +] + [[package]] name = "arbitrary" version = "1.4.1" @@ -92,7 +114,7 @@ dependencies = [ "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "parking_lot", "percent-encoding", "windows-sys 0.60.2", @@ -458,7 +480,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] [[package]] @@ -467,6 +489,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bit_field" version = "0.10.2" @@ -481,11 +512,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -497,6 +528,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -506,6 +546,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.5.1" @@ -616,7 +665,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "cairo-sys-rs", "glib", "libc", @@ -637,9 +686,9 @@ dependencies = [ [[package]] name = "calamine" -version = "0.34.0" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ae05a4e39297eecf9a994210d27501318c37a9318201f8e11050add82bb6f0" +checksum = "8822fe6253ca47aa5ad9a3be09f6fe7cd20c6a74e41b0aa42e8f4e3d523508df" dependencies = [ "atoi_simd", "byteorder", @@ -700,7 +749,16 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" dependencies = [ - "cipher", + "cipher 0.4.4", +] + +[[package]] +name = "cbc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98db6aeaef0eeef2c1e3ce9a27b739218825dae116076352ac3777076aa22225" +dependencies = [ + "cipher 0.5.1", ] [[package]] @@ -762,7 +820,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core 0.10.0", + "rand_core", ] [[package]] @@ -786,8 +844,18 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", - "inout", + "crypto-common 0.1.6", + "inout 0.1.3", +] + +[[package]] +name = "cipher" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34d8227fe1ba289043aeb13792056ff80fd6de1a9f49137a5f499de8e8c78ea" +dependencies = [ + "crypto-common 0.2.1", + "inout 0.2.2", ] [[package]] @@ -808,6 +876,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "codepage" version = "0.1.2" @@ -862,6 +936,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -878,16 +958,6 @@ dependencies = [ "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.0" @@ -910,10 +980,10 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ - "bitflags 2.6.0", - "core-foundation 0.10.0", + "bitflags 2.11.1", + "core-foundation", "core-graphics-types", - "foreign-types 0.5.0", + "foreign-types", "libc", ] @@ -923,10 +993,10 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.6.0", - "core-foundation 0.10.0", + "bitflags 2.11.1", + "core-foundation", "core-graphics-types", - "foreign-types 0.5.0", + "foreign-types", "libc", ] @@ -936,11 +1006,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.6.0", - "core-foundation 0.10.0", + "bitflags 2.11.1", + "core-foundation", "libc", ] +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -1033,6 +1109,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "cssparser" version = "0.36.0" @@ -1072,6 +1157,15 @@ version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "darling" version = "0.20.10" @@ -1126,15 +1220,30 @@ dependencies = [ [[package]] name = "dbus-secret-service" -version = "4.0.2" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1caa0c241c01ad8d99a78d553567d38f873dd3ac16eca33a5370d650ab25584e" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" dependencies = [ + "aes 0.8.4", + "block-padding 0.3.3", + "cbc 0.1.2", "dbus", - "futures-util", + "fastrand", + "hkdf", "num", "once_cell", - "rand 0.8.5", + "sha2 0.10.8", + "zeroize", +] + +[[package]] +name = "dbus-secret-service-keyring-store" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21d8f54da401bb5eb2a4d873ac4b359f4a599df2ca8634bb5b8c045e5ee78757" +dependencies = [ + "dbus-secret-service", + "keyring-core", ] [[package]] @@ -1211,11 +1320,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.6", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.1", + "ctutils", +] + [[package]] name = "dirs" version = "6.0.0" @@ -1243,7 +1364,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -1577,15 +1698,6 @@ 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" @@ -1593,7 +1705,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared 0.3.1", + "foreign-types-shared", ] [[package]] @@ -1607,12 +1719,6 @@ dependencies = [ "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" @@ -1871,10 +1977,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "wasm-bindgen", ] [[package]] @@ -1900,7 +2004,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "rand_core 0.10.0", + "rand_core", "wasip2", "wasip3", ] @@ -1953,7 +2057,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "futures-channel", "futures-core", "futures-executor", @@ -2143,12 +2247,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "hermit-abi" version = "0.5.2" @@ -2161,13 +2259,31 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + [[package]] name = "hmac" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", ] [[package]] @@ -2226,6 +2342,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.9.0" @@ -2265,22 +2390,6 @@ dependencies = [ "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" @@ -2299,11 +2408,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", - "system-configuration", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] @@ -2561,10 +2668,20 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ - "block-padding", + "block-padding 0.3.3", "generic-array", ] +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding 0.4.2", + "hybrid-array", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -2718,23 +2835,18 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "serde", "unicode-segmentation", ] [[package]] -name = "keyring" -version = "3.6.2" +name = "keyring-core" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1961983669d57bdfe6c0f3ef8e4c229b5ef751afcc7d87e4271d2f71f6ccfa8b" +checksum = "fb1e621458ca9c51aa110bd0339d4751a056b9576bf1253aee1aa560dda0fc9d" dependencies = [ - "byteorder", - "dbus-secret-service", "log", - "security-framework 2.11.1", - "security-framework 3.5.1", - "windows-sys 0.59.0", ] [[package]] @@ -2811,7 +2923,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", - "windows-targets 0.48.5", + "windows-targets 0.52.6", ] [[package]] @@ -2820,7 +2932,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "libc", ] @@ -2873,12 +2985,6 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" -[[package]] -name = "lru-slab" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" - [[package]] name = "lzma-rs" version = "0.3.0" @@ -2948,7 +3054,8 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" name = "mindwork-ai-studio" version = "26.5.4" dependencies = [ - "aes", + "aes 0.9.0", + "apple-native-keyring-store", "arboard", "async-stream", "axum", @@ -2956,26 +3063,26 @@ dependencies = [ "base64 0.22.1", "bytes", "calamine", - "cbc", + "cbc 0.2.0", "cfg-if", + "dbus-secret-service-keyring-store", "file-format", "flexi_logger", "futures", - "hmac", - "keyring", + "hmac 0.13.0", + "keyring-core", "log", "once_cell", - "pbkdf2", + "pbkdf2 0.13.0", "pdfium-render", "pptx-to-md", - "rand 0.10.1", - "rand_chacha 0.10.0", + "rand", + "rand_chacha", "rcgen", - "reqwest", "rustls", "serde", "serde_json", - "sha2", + "sha2 0.11.0", "strum_macros", "sys-locale", "sysinfo", @@ -2990,6 +3097,7 @@ dependencies = [ "tempfile", "tokio", "tokio-stream", + "windows-native-keyring-store", "windows-registry", ] @@ -3027,14 +3135,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ - "hermit-abi 0.3.9", "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3050,7 +3157,7 @@ dependencies = [ "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "once_cell", "png 0.18.1", "serde", @@ -3058,30 +3165,13 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "native-tls" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe 0.1.5", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "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.6.0", + "bitflags 2.11.1", "jni-sys", "log", "ndk-sys", @@ -3266,12 +3356,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "block2 0.6.2", "objc2 0.6.4", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", ] [[package]] @@ -3280,9 +3370,9 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c1948a9be5f469deadbd6bcb86ad7ff9e47b4f632380139722f7d9840c0d42c" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "objc2 0.6.4", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", ] [[package]] @@ -3292,7 +3382,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4" dependencies = [ "objc2 0.6.4", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", ] [[package]] @@ -3301,7 +3391,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "dispatch2", "objc2 0.6.4", ] @@ -3312,7 +3402,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "objc2 0.6.4", "objc2-core-foundation", "objc2-io-surface", @@ -3325,7 +3415,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ffa6bea72bf42c78b0b34e89c0bafac877d5f80bf91e159a5d96ea7f693ca56" dependencies = [ "objc2 0.6.4", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", ] [[package]] @@ -3335,7 +3425,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d31f4c5b5192304996badc466aeadffe1411d73a9bbd3b18b6b2ee9d048b07bd" dependencies = [ "objc2 0.6.4", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", ] [[package]] @@ -3359,7 +3449,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "block2 0.5.1", "libc", "objc2 0.5.2", @@ -3367,11 +3457,11 @@ dependencies = [ [[package]] name = "objc2-foundation" -version = "0.3.0" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "block2 0.6.2", "libc", "objc2 0.6.4", @@ -3394,7 +3484,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "objc2 0.6.4", "objc2-core-foundation", ] @@ -3405,22 +3495,33 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", ] +[[package]] +name = "objc2-open-directory" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb82bed227edf5201dfedf072bba4015a33d3d4a98519837295a90f0a23f676d" +dependencies = [ + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-osa-kit" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1ac59da3ceebc4a82179b35dc550431ad9458f9cc326e053f49ba371ce76c5a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "objc2 0.6.4", "objc2-app-kit", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", ] [[package]] @@ -3429,7 +3530,7 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "block2 0.5.1", "objc2 0.5.2", "objc2-foundation 0.2.2", @@ -3442,10 +3543,10 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb3794501bb1bee12f08dcad8c61f2a5875791ad1c6f47faa71a0f033f20071" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "objc2 0.6.4", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", ] [[package]] @@ -3454,7 +3555,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "777a571be14a42a3990d4ebedaeb8b54cd17377ec21b92e8200ac03797b3bee1" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "block2 0.6.2", "objc2 0.6.4", "objc2-cloud-kit", @@ -3463,7 +3564,7 @@ dependencies = [ "objc2-core-graphics", "objc2-core-image", "objc2-core-location", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "objc2-quartz-core 0.3.0", "objc2-user-notifications", ] @@ -3475,7 +3576,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "670fe793adbf3b5e93686d48a05a7ed7ee53dfa65d106ced4805fae8969059b2" dependencies = [ "objc2 0.6.4", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", ] [[package]] @@ -3484,12 +3585,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "block2 0.6.2", "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", ] [[package]] @@ -3519,66 +3620,12 @@ dependencies = [ "pathdiff", ] -[[package]] -name = "openssl" -version = "0.10.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" -dependencies = [ - "bitflags 2.6.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.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - [[package]] name = "openssl-probe" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f50d9b3dabb09ecd771ad0aa242ca6894994c130308ca3d7684634df8037391" -[[package]] -name = "openssl-src" -version = "300.3.1+3.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" -dependencies = [ - "cc", -] - -[[package]] -name = "openssl-sys" -version = "0.9.112" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" -dependencies = [ - "cc", - "libc", - "openssl-src", - "pkg-config", - "vcpkg", -] - [[package]] name = "option-ext" version = "0.2.0" @@ -3612,7 +3659,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b" dependencies = [ "objc2 0.6.4", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "objc2-osa-kit", "serde", "serde_json", @@ -3685,17 +3732,27 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ - "digest", - "hmac", + "digest 0.10.7", + "hmac 0.12.1", +] + +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.3", + "hmac 0.13.0", ] [[package]] name = "pdfium-render" -version = "0.8.37" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6553f6604a52b3203db7b4e9d51eb4dd193cf455af9e56d40cab6575b547b679" +checksum = "076dd8f3a6c7da9298ddffbcc0d5a109f89caf967fa4871c9a172d5b3498b35b" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "bytemuck", "bytes", "chrono", @@ -3845,7 +3902,7 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "crc32fast", "fdeflate", "flate2", @@ -3860,7 +3917,7 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi 0.5.2", + "hermit-abi", "pin-project-lite", "rustix 1.1.4", "windows-sys 0.61.2", @@ -3998,62 +4055,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "quinn" -version = "0.11.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" -dependencies = [ - "bytes", - "cfg_aliases", - "pin-project-lite", - "quinn-proto", - "quinn-udp", - "rustc-hash", - "rustls", - "socket2", - "thiserror 2.0.12", - "tokio", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-proto" -version = "0.11.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" -dependencies = [ - "aws-lc-rs", - "bytes", - "getrandom 0.3.1", - "lru-slab", - "rand 0.9.1", - "ring", - "rustc-hash", - "rustls", - "rustls-pki-types", - "slab", - "thiserror 2.0.12", - "tinyvec", - "tracing", - "web-time", -] - -[[package]] -name = "quinn-udp" -version = "0.5.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" -dependencies = [ - "cfg_aliases", - "libc", - "once_cell", - "socket2", - "tracing", - "windows-sys 0.60.2", -] - [[package]] name = "quote" version = "1.0.36" @@ -4069,27 +4070,6 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" -[[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.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.0", -] - [[package]] name = "rand" version = "0.10.1" @@ -4098,27 +4078,7 @@ checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ "chacha20", "getrandom 0.4.2", - "rand_core 0.10.0", -] - -[[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_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.0", + "rand_core", ] [[package]] @@ -4128,26 +4088,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e6af7f3e25ded52c41df4e0b1af2d047e45896c2f3281792ed68a1c243daedb" dependencies = [ "ppv-lite86", - "rand_core 0.10.0", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.15", -] - -[[package]] -name = "rand_core" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b08f3c9802962f7e1b25113931d94f43ed9725bebc59db9d0c3e9a23b67e15ff" -dependencies = [ - "getrandom 0.3.1", - "zerocopy", + "rand_core", ] [[package]] @@ -4184,9 +4125,9 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.14.7" +version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" dependencies = [ "pem", "ring", @@ -4211,7 +4152,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", ] [[package]] @@ -4262,24 +4203,18 @@ checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64 0.22.1", "bytes", - "encoding_rs", "futures-core", "futures-util", - "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", - "quinn", "rustls", "rustls-pki-types", "rustls-platform-verifier", @@ -4287,7 +4222,6 @@ dependencies = [ "serde_json", "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls", "tokio-util", "tower", @@ -4316,7 +4250,7 @@ dependencies = [ "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "raw-window-handle", "wasm-bindgen", "wasm-bindgen-futures", @@ -4374,7 +4308,7 @@ version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.4.14", @@ -4387,7 +4321,7 @@ version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "errno", "libc", "linux-raw-sys 0.12.1", @@ -4415,10 +4349,10 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.0", + "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.1", + "security-framework", ] [[package]] @@ -4427,7 +4361,6 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ - "web-time", "zeroize", ] @@ -4437,7 +4370,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ - "core-foundation 0.10.0", + "core-foundation", "core-foundation-sys", "jni", "log", @@ -4446,7 +4379,7 @@ dependencies = [ "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", - "security-framework 3.5.1", + "security-framework", "security-framework-sys", "webpki-root-certs", "windows-sys 0.61.2", @@ -4535,25 +4468,12 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.11.1" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ - "bitflags 2.6.0", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" -dependencies = [ - "bitflags 2.6.0", - "core-foundation 0.10.0", + "bitflags 2.11.1", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -4561,9 +4481,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.15.0" +version = "2.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" dependencies = [ "core-foundation-sys", "libc", @@ -4575,7 +4495,7 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "cssparser", "derive_more", "log", @@ -4784,7 +4704,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures 0.2.12", - "digest", + "digest 0.10.7", ] [[package]] @@ -4795,7 +4715,18 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures 0.2.12", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -4852,12 +4783,12 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4869,7 +4800,7 @@ dependencies = [ "bytemuck", "cfg_aliases", "core-graphics 0.24.0", - "foreign-types 0.5.0", + "foreign-types", "js-sys", "log", "objc2 0.5.2", @@ -5026,39 +4957,19 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.38.4" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" +checksum = "a4deba334e1190ba7cb498327affa11e5ece10d26a30ab2f27fcf09504b8d8b6" dependencies = [ "libc", "memchr", "ntapi", "objc2-core-foundation", "objc2-io-kit", + "objc2-open-directory", "windows 0.62.2", ] -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags 2.6.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" @@ -5078,9 +4989,9 @@ version = "0.35.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33f7f9e486ade65fcf1e45c440f9236c904f5c1002cdc7fc6ae582777345ce4" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "block2 0.6.2", - "core-foundation 0.10.0", + "core-foundation", "core-graphics 0.25.0", "crossbeam-channel", "dbus", @@ -5097,7 +5008,7 @@ dependencies = [ "ndk-sys", "objc2 0.6.4", "objc2-app-kit", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "objc2-ui-kit", "once_cell", "parking_lot", @@ -5164,7 +5075,7 @@ dependencies = [ "muda", "objc2 0.6.4", "objc2-app-kit", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "objc2-ui-kit", "objc2-web-kit", "percent-encoding", @@ -5229,7 +5140,7 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2", + "sha2 0.10.8", "syn 2.0.117", "tauri-utils", "thiserror 2.0.12", @@ -5297,7 +5208,7 @@ dependencies = [ "dunce", "glob", "log", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "percent-encoding", "schemars", "serde", @@ -5335,7 +5246,7 @@ dependencies = [ "dunce", "glob", "objc2-app-kit", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "open", "schemars", "serde", @@ -5408,7 +5319,7 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "log", "serde", "serde_json", @@ -5632,26 +5543,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", @@ -5665,25 +5561,15 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" 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.1" @@ -5860,7 +5746,7 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "bytes", "futures-util", "http", @@ -5930,7 +5816,7 @@ dependencies = [ "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "once_cell", "png 0.18.1", "serde", @@ -5958,9 +5844,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.17.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "uds_windows" @@ -6100,12 +5986,6 @@ dependencies = [ "serde", ] -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "vecmath" version = "1.0.0" @@ -6295,7 +6175,7 @@ version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", "hashbrown 0.15.2", "indexmap 2.14.0", "semver", @@ -6311,16 +6191,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "web_atoms" version = "0.2.4" @@ -6468,7 +6338,7 @@ dependencies = [ "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "raw-window-handle", "windows-sys 0.59.0", "windows-version", @@ -6608,6 +6478,19 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-native-keyring-store" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5fd986f648459dd29aa252ed3a5ad11a60c0b1251bf81625fb03a86c69d274e" +dependencies = [ + "byteorder", + "keyring-core", + "regex", + "windows-sys 0.61.2", + "zeroize", +] + [[package]] name = "windows-numerics" version = "0.2.0" @@ -7050,7 +6933,7 @@ version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.11.1", ] [[package]] @@ -7091,7 +6974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", - "bitflags 2.6.0", + "bitflags 2.11.1", "indexmap 2.14.0", "log", "serde", @@ -7157,13 +7040,13 @@ dependencies = [ "objc2 0.6.4", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.0", + "objc2-foundation 0.3.2", "objc2-ui-kit", "objc2-web-kit", "once_cell", "percent-encoding", "raw-window-handle", - "sha2", + "sha2 0.10.8", "soup3", "tao-macros", "thiserror 2.0.12", @@ -7261,10 +7144,11 @@ dependencies = [ [[package]] name = "yasna" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" dependencies = [ + "bit-vec 0.9.1", "time", ] @@ -7353,26 +7237,6 @@ dependencies = [ "zvariant", ] -[[package]] -name = "zerocopy" -version = "0.8.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa91407dacce3a68c56de03abe2760159582b846c6a4acd2f456618087f12713" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06718a168365cad3d5ff0bb133aad346959a2074bd4a85c121255a11304a8626" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - [[package]] name = "zerofrom" version = "0.1.5" @@ -7442,7 +7306,7 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c03817464f64e23f6f37574b4fdc8cf65925b5bfd2b0f2aedf959791941f88" dependencies = [ - "aes", + "aes 0.8.4", "arbitrary", "bzip2", "constant_time_eq", @@ -7451,11 +7315,11 @@ dependencies = [ "deflate64", "flate2", "getrandom 0.3.1", - "hmac", + "hmac 0.12.1", "indexmap 2.14.0", "lzma-rs", "memchr", - "pbkdf2", + "pbkdf2 0.12.2", "sha1", "time", "xz2", diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index c500df0c..304d0332 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -16,9 +16,9 @@ tauri-plugin-dialog = "2.7.1" tauri-plugin-opener = "2.5.4" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" -keyring = { version = "3.6.2", features = ["apple-native", "windows-native", "sync-secret-service"] } +keyring-core = "1.0.0" arboard = "3.6.1" -tokio = { version = "1.50.0", features = ["rt", "rt-multi-thread", "macros", "process"] } +tokio = { version = "1.52.3", features = ["rt", "rt-multi-thread", "macros", "process"] } tokio-stream = "0.1.18" futures = "0.3.32" async-stream = "0.3.6" @@ -31,31 +31,32 @@ rustls = { version = "0.23.28", default-features = false, features = ["aws_lc_rs rand = "0.10.1" rand_chacha = "0.10.0" base64 = "0.22.1" -aes = "0.8.4" -cbc = "0.1.2" -pbkdf2 = "0.12.2" -hmac = "0.12.1" -sha2 = "0.10.8" -rcgen = { version = "0.14.7", features = ["pem"] } +aes = "0.9.0" +cbc = "0.2.0" +pbkdf2 = "0.13.0" +hmac = "0.13.0" +sha2 = "0.11.0" +rcgen = { version = "0.14.8", features = ["pem"] } file-format = "0.29.0" -calamine = "0.34.0" -pdfium-render = "0.8.37" +calamine = "0.35.0" +pdfium-render = "0.9.1" sys-locale = "0.3.2" cfg-if = "1.0.4" pptx-to-md = "0.4.0" tempfile = "3.27.0" strum_macros = "0.28.0" -sysinfo = "0.38.4" - -# Fixes security vulnerability downstream, where the upstream is not fixed yet: -bytes = "1.11.1" # -> almost every dependency - -[target.'cfg(target_os = "linux")'.dependencies] -# See issue https://github.com/tauri-apps/tauri/issues/4470 -reqwest = { version = "0.13.2", features = ["native-tls-vendored"] } +sysinfo = "0.39.1" +bytes = "1.11.1" [target.'cfg(target_os = "windows")'.dependencies] windows-registry = "0.6.1" +windows-native-keyring-store = "1.0.0" + +[target.'cfg(target_os = "macos")'.dependencies] +apple-native-keyring-store = { version = "1.0.0", features = ["keychain"] } + +[target.'cfg(target_os = "linux")'.dependencies] +dbus-secret-service-keyring-store = { version = "1.0.0", features = ["crypto-rust"] } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-global-shortcut = "2" diff --git a/runtime/src/encryption.rs b/runtime/src/encryption.rs index 2c7828b3..22e58806 100644 --- a/runtime/src/encryption.rs +++ b/runtime/src/encryption.rs @@ -2,7 +2,7 @@ use std::fmt; use std::time::Instant; use base64::Engine; use base64::prelude::BASE64_STANDARD; -use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}; +use aes::cipher::{block_padding::Pkcs7, BlockModeDecrypt, BlockModeEncrypt, KeyIvInit}; use hmac::Hmac; use log::{error, info}; use once_cell::sync::Lazy; @@ -107,7 +107,7 @@ impl Encryption { let mut buffer = vec![0u8; data.len() + 16]; buffer[..data.len()].copy_from_slice(data); let encrypted = cipher - .encrypt_padded_mut::<Pkcs7>(&mut buffer, data.len()) + .encrypt_padded::<Pkcs7>(&mut buffer, data.len()) .map_err(|e| format!("Error encrypting data: {e}"))?; let mut result = BASE64_STANDARD.encode(self.secret_key_salt); result.push_str(&BASE64_STANDARD.encode(encrypted)); @@ -130,7 +130,7 @@ impl Encryption { let cipher = Aes256CbcDec::new(&self.key.into(), &self.iv.into()); let mut buffer = encrypted.to_vec(); let decrypted = cipher - .decrypt_padded_mut::<Pkcs7>(&mut buffer) + .decrypt_padded::<Pkcs7>(&mut buffer) .map_err(|e| format!("Error decrypting data: {e}"))?; String::from_utf8(decrypted.to_vec()).map_err(|e| format!("Error converting decrypted data to string: {}", e)) diff --git a/runtime/src/main.rs b/runtime/src/main.rs index 84d280fe..c03f26dc 100644 --- a/runtime/src/main.rs +++ b/runtime/src/main.rs @@ -10,6 +10,7 @@ use mindwork_ai_studio::environment::is_dev; use mindwork_ai_studio::log::init_logging; use mindwork_ai_studio::metadata::MetaData; use mindwork_ai_studio::runtime_api::start_runtime_api; +use mindwork_ai_studio::secret::init_secret_store; #[tokio::main] async fn main() { @@ -41,6 +42,7 @@ async fn main() { info!("Running in production mode."); } + init_secret_store(); generate_runtime_certificate(); start_runtime_api(); diff --git a/runtime/src/secret.rs b/runtime/src/secret.rs index 2f074a62..c587c4d4 100644 --- a/runtime/src/secret.rs +++ b/runtime/src/secret.rs @@ -1,11 +1,43 @@ -use keyring::Entry; -use log::{error, info, warn}; use axum::Json; +use keyring_core::{Entry, Error as KeyringError}; +use log::{error, info, warn}; use serde::{Deserialize, Serialize}; -use keyring::error::Error::NoEntry; use crate::api_token::APIToken; use crate::encryption::{EncryptedText, ENCRYPTION}; +/// Initializes the native credential store used by keyring-core. +pub fn init_secret_store() { + cfg_if::cfg_if! { + if #[cfg(target_os = "macos")] { + match apple_native_keyring_store::keychain::Store::new() { + Ok(store) => { + keyring_core::set_default_store(store); + info!(Source = "Secret Store"; "Initialized the macOS Keychain credential store."); + }, + Err(e) => error!(Source = "Secret Store"; "Failed to initialize the macOS Keychain credential store: {e}."), + } + } else if #[cfg(target_os = "windows")] { + match windows_native_keyring_store::Store::new() { + Ok(store) => { + keyring_core::set_default_store(store); + info!(Source = "Secret Store"; "Initialized the Windows Credential Manager store."); + }, + Err(e) => error!(Source = "Secret Store"; "Failed to initialize the Windows Credential Manager store: {e}."), + } + } else if #[cfg(target_os = "linux")] { + match dbus_secret_service_keyring_store::Store::new() { + Ok(store) => { + keyring_core::set_default_store(store); + info!(Source = "Secret Store"; "Initialized the DBus Secret Service credential store."); + }, + Err(e) => error!(Source = "Secret Store"; "Failed to initialize the DBus Secret Service credential store: {e}."), + } + } else { + warn!(Source = "Secret Store"; "No native credential store is configured for this platform."); + } + } +} + /// Stores a secret in the secret store using the operating system's keyring. pub async fn store_secret(_token: APIToken, request: Json<StoreSecret>) -> Json<StoreSecretResponse> { let user_name = request.user_name.as_str(); @@ -21,7 +53,16 @@ pub async fn store_secret(_token: APIToken, request: Json<StoreSecret>) -> Json< }; let service = format!("mindwork-ai-studio::{}", request.destination); - let entry = Entry::new(service.as_str(), user_name).unwrap(); + let entry = match Entry::new(service.as_str(), user_name) { + Ok(entry) => entry, + Err(e) => { + error!(Source = "Secret Store"; "Failed to create secret entry for {service} and user {user_name}: {e}."); + return Json(StoreSecretResponse { + success: false, + issue: e.to_string(), + }); + }, + }; let result = entry.set_password(decrypted_text.as_str()); match result { Ok(_) => { @@ -61,7 +102,20 @@ pub struct StoreSecretResponse { pub async fn get_secret(_token: APIToken, request: Json<RequestSecret>) -> Json<RequestedSecret> { let user_name = request.user_name.as_str(); let service = format!("mindwork-ai-studio::{}", request.destination); - let entry = Entry::new(service.as_str(), user_name).unwrap(); + let entry = match Entry::new(service.as_str(), user_name) { + Ok(entry) => entry, + Err(e) => { + if !request.is_trying { + error!(Source = "Secret Store"; "Failed to create secret entry for '{service}' and user '{user_name}': {e}."); + } + + return Json(RequestedSecret { + success: false, + secret: EncryptedText::new(String::from("")), + issue: format!("Failed to create secret entry for '{service}' and user '{user_name}': {e}"), + }); + }, + }; let secret = entry.get_password(); match secret { Ok(s) => { @@ -121,7 +175,17 @@ pub struct RequestedSecret { pub async fn delete_secret(_token: APIToken, request: Json<RequestSecret>) -> Json<DeleteSecretResponse> { let user_name = request.user_name.as_str(); let service = format!("mindwork-ai-studio::{}", request.destination); - let entry = Entry::new(service.as_str(), user_name).unwrap(); + let entry = match Entry::new(service.as_str(), user_name) { + Ok(entry) => entry, + Err(e) => { + error!(Source = "Secret Store"; "Failed to create secret entry for {service} and user {user_name}: {e}."); + return Json(DeleteSecretResponse { + success: false, + was_entry_found: false, + issue: e.to_string(), + }); + }, + }; let result = entry.delete_credential(); match result { @@ -134,7 +198,7 @@ pub async fn delete_secret(_token: APIToken, request: Json<RequestSecret>) -> Js }) }, - Err(NoEntry) => { + Err(KeyringError::NoEntry) => { warn!(Source = "Secret Store"; "No secret for {service} and user {user_name} was found."); Json(DeleteSecretResponse { success: true, From fc3c000de69442bac8330046d47c1a3ead4bf331 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Fri, 15 May 2026 18:13:30 +0200 Subject: [PATCH 13/21] Improved pipeline (#763) --- .github/workflows/build-and-release.yml | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 2fe8cc6d..dc639073 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -12,6 +12,10 @@ on: - synchronize - reopened +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && (github.event.action != 'labeled' || github.event.label.name == 'run-pipeline') && github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' && (github.event.action != 'labeled' || github.event.label.name == 'run-pipeline') }} + env: RETENTION_INTERMEDIATE_ASSETS: 1 RETENTION_RELEASE_ASSETS: 30 @@ -37,6 +41,8 @@ jobs: id: determine env: EVENT_NAME: ${{ github.event_name }} + PR_ACTION: ${{ github.event.action }} + ACTION_LABEL_NAME: ${{ github.event.label.name }} REF: ${{ github.ref }} PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ' ') }} PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} @@ -55,6 +61,11 @@ jobs: is_internal_pr=true fi + has_run_pipeline_label=false + if [[ " $PR_LABELS " == *" run-pipeline "* ]]; then + has_run_pipeline_label=true + fi + if [[ "$REF" == refs/tags/v* ]]; then is_release=true build_enabled=true @@ -65,13 +76,21 @@ jobs: build_enabled=true artifact_retention_days=7 skip_reason="" - elif [[ "$EVENT_NAME" == "pull_request" && " $PR_LABELS " == *" run-pipeline "* ]]; then + elif [[ "$EVENT_NAME" == "pull_request" && "$PR_ACTION" == "labeled" && "$ACTION_LABEL_NAME" == "run-pipeline" ]]; then is_labeled_pr=true is_pr_build=true build_enabled=true artifact_retention_days=3 skip_reason="" - elif [[ "$EVENT_NAME" == "pull_request" && " $PR_LABELS " != *" run-pipeline "* ]]; then + elif [[ "$EVENT_NAME" == "pull_request" && "$PR_ACTION" != "labeled" && "$has_run_pipeline_label" == "true" ]]; then + is_labeled_pr=true + is_pr_build=true + build_enabled=true + artifact_retention_days=3 + skip_reason="" + elif [[ "$EVENT_NAME" == "pull_request" && "$PR_ACTION" == "labeled" ]]; then + skip_reason="Build disabled: label '${ACTION_LABEL_NAME}' is not 'run-pipeline'." + elif [[ "$EVENT_NAME" == "pull_request" && "$has_run_pipeline_label" != "true" ]]; then skip_reason="Build disabled: PR does not have the required 'run-pipeline' label." fi @@ -685,11 +704,9 @@ jobs: uses: actions/cache@v4 with: path: | - ~/.cargo/bin ~/.cargo/git/db/ ~/.cargo/registry/index/ ~/.cargo/registry/cache/ - ~/.rustup/toolchains runtime/target key: target-${{ matrix.dotnet_runtime }}-rust-${{ env.RUST_VERSION }} From 8f0effd25bb8a6f16dc6ce679b87230096d2f8e3 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Sat, 16 May 2026 17:38:38 +0200 Subject: [PATCH 14/21] Added dedicated Tauri tool cache (#764) --- .github/workflows/build-and-release.yml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index dc639073..b290ba11 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -716,6 +716,12 @@ jobs: with: toolchain: ${{ env.RUST_VERSION }} targets: ${{ matrix.rust_target }} + + - name: Cache Tauri CLI + uses: actions/cache@v4 + with: + path: ~/.cargo-tauri-cli + key: tauri-cli-v2-${{ runner.os }}-${{ runner.arch }} - name: Setup dependencies (Ubuntu-specific, x86) if: matrix.platform == 'ubuntu-22.04' && contains(matrix.rust_target, 'x86_64') @@ -732,8 +738,11 @@ jobs: - name: Setup Tauri (Unix) if: matrix.platform != 'windows-latest' run: | + echo "$HOME/.cargo-tauri-cli/bin" >> "$GITHUB_PATH" + export PATH="$HOME/.cargo-tauri-cli/bin:$PATH" + if ! cargo tauri --version 2>/dev/null | grep -Eq '^tauri-cli 2\.'; then - cargo install tauri-cli --version "^2.11.0" --locked --force + cargo install tauri-cli --version "^2.11.0" --locked --force --root "$HOME/.cargo-tauri-cli" else echo "Tauri CLI v2 is already installed" fi @@ -741,9 +750,12 @@ jobs: - name: Setup Tauri (Windows) if: matrix.platform == 'windows-latest' run: | + "$env:USERPROFILE\.cargo-tauri-cli\bin" >> $env:GITHUB_PATH + $env:PATH = "$env:USERPROFILE\.cargo-tauri-cli\bin;$env:PATH" + $tauriVersion = cargo tauri --version 2>$null if (-not $tauriVersion -or $tauriVersion -notmatch '^tauri-cli 2\.') { - cargo install tauri-cli --version "^2.11.0" --locked --force + cargo install tauri-cli --version "^2.11.0" --locked --force --root "$env:USERPROFILE\.cargo-tauri-cli" } else { Write-Output "Tauri CLI v2 is already installed" } From 91cfe8dcd08f84c36db365b6d5f45c9f994dcf0f Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Sat, 16 May 2026 18:27:16 +0200 Subject: [PATCH 15/21] Fixed & improved pandoc handling (#762) --- .../Assistants/I18N/allTexts.lua | 22 +- .../plugin.lua | 22 +- .../plugin.lua | 22 +- app/MindWork AI Studio/Tools/Pandoc.cs | 218 +++++++++++++++--- .../Tools/PandocProcessBuilder.cs | 66 ++++++ .../wwwroot/changelog/v26.5.5.md | 2 + runtime/src/app_window.rs | 2 +- runtime/src/pandoc.rs | 100 ++++++-- 8 files changed, 384 insertions(+), 70 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 314d30c2..73b7b83b 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -6973,6 +6973,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T816853779"] = "Failed -- Failed to retrieve the authentication methods: the ERI server did not return a valid response. UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Failed to retrieve the authentication methods: the ERI server did not return a valid response." +-- AI Studio couldn't install Pandoc because the archive was not found. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio couldn't install Pandoc because the archive was not found." + +-- Pandoc doesn't seem to be installed. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1090474732"] = "Pandoc doesn't seem to be installed." + -- Was not able to validate the Pandoc installation. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1364844008"] = "Was not able to validate the Pandoc installation." @@ -6994,20 +7000,20 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2550598062"] = "Pandoc v{0} is instal -- Pandoc v{0} is installed, but it does not match the required version (v{1}). UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2555465873"] = "Pandoc v{0} is installed, but it does not match the required version (v{1})." --- Pandoc was not installed successfully, because the archive was not found. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T34210248"] = "Pandoc was not installed successfully, because the archive was not found." +-- AI Studio couldn't install Pandoc because the archive type is unknown. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3492710362"] = "AI Studio couldn't install Pandoc because the archive type is unknown." -- Pandoc is not available on the system or the process had issues. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc is not available on the system or the process had issues." --- Pandoc was not installed successfully, because the archive type is unknown. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3962211670"] = "Pandoc was not installed successfully, because the archive type is unknown." +-- AI Studio couldn't install Pandoc because the executable was not found in the archive. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T403983772"] = "AI Studio couldn't install Pandoc because the executable was not found in the archive." --- It seems that Pandoc is not installed. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "It seems that Pandoc is not installed." +-- AI Studio couldn't find the latest Pandoc version and will install version {0} instead. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T695293525"] = "AI Studio couldn't find the latest Pandoc version and will install version {0} instead." --- The latest Pandoc version was not found, installing version {0} instead. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "The latest Pandoc version was not found, installing version {0} instead." +-- AI Studio couldn't install Pandoc. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio couldn't install Pandoc." -- Pandoc is required for Microsoft Word export. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc is required for Microsoft Word export." diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index fb4216bd..32598b6f 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -6975,6 +6975,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T816853779"] = "Fehler -- Failed to retrieve the authentication methods: the ERI server did not return a valid response. UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Fehler beim Abrufen der Authentifizierungsmethoden: Der ERI-Server hat keine gültige Antwort zurückgegeben." +-- AI Studio couldn't install Pandoc because the archive was not found. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio konnte Pandoc nicht installieren, da das Archiv nicht gefunden wurde." + +-- Pandoc doesn't seem to be installed. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1090474732"] = "Pandoc scheint nicht installiert zu sein." + -- Was not able to validate the Pandoc installation. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1364844008"] = "Die Pandoc-Installation konnte nicht überprüft werden." @@ -6996,20 +7002,20 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2550598062"] = "Pandoc v{0} ist insta -- Pandoc v{0} is installed, but it does not match the required version (v{1}). UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2555465873"] = "Pandoc v{0} ist installiert, entspricht aber nicht der benötigten Version (v{1})." --- Pandoc was not installed successfully, because the archive was not found. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T34210248"] = "Pandoc wurde nicht erfolgreich installiert, da das Archiv nicht gefunden wurde." +-- AI Studio couldn't install Pandoc because the archive type is unknown. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3492710362"] = "AI Studio konnte Pandoc nicht installieren, da der Archivtyp unbekannt ist." -- Pandoc is not available on the system or the process had issues. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc ist auf dem System nicht verfügbar oder der Vorgang ist auf Probleme gestoßen." --- Pandoc was not installed successfully, because the archive type is unknown. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3962211670"] = "Pandoc wurde nicht erfolgreich installiert, da der Archivtyp unbekannt ist." +-- AI Studio couldn't install Pandoc because the executable was not found in the archive. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T403983772"] = "AI Studio konnte Pandoc nicht installieren, da die ausführbare Datei im Archiv nicht gefunden wurde." --- It seems that Pandoc is not installed. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "Es scheint, dass Pandoc nicht installiert ist." +-- AI Studio couldn't find the latest Pandoc version and will install version {0} instead. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T695293525"] = "AI Studio konnte die neueste Pandoc-Version nicht finden und installiert stattdessen Version {0}." --- The latest Pandoc version was not found, installing version {0} instead. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "Die neueste Pandoc-Version wurde nicht gefunden, stattdessen wird Version {0} installiert." +-- AI Studio couldn't install Pandoc. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio konnte Pandoc nicht installieren." -- Pandoc is required for Microsoft Word export. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc wird für den Export nach Microsoft Word benötigt." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 26be03ee..c837f96d 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -6975,6 +6975,12 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T816853779"] = "Failed -- Failed to retrieve the authentication methods: the ERI server did not return a valid response. UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T984407320"] = "Failed to retrieve the authentication methods: the ERI server did not return a valid response." +-- AI Studio couldn't install Pandoc because the archive was not found. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1059477764"] = "AI Studio couldn't install Pandoc because the archive was not found." + +-- Pandoc doesn't seem to be installed. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1090474732"] = "Pandoc doesn't seem to be installed." + -- Was not able to validate the Pandoc installation. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T1364844008"] = "Was not able to validate the Pandoc installation." @@ -6996,20 +7002,20 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2550598062"] = "Pandoc v{0} is instal -- Pandoc v{0} is installed, but it does not match the required version (v{1}). UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T2555465873"] = "Pandoc v{0} is installed, but it does not match the required version (v{1})." --- Pandoc was not installed successfully, because the archive was not found. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T34210248"] = "Pandoc was not installed successfully, because the archive was not found." +-- AI Studio couldn't install Pandoc because the archive type is unknown. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3492710362"] = "AI Studio couldn't install Pandoc because the archive type is unknown." -- Pandoc is not available on the system or the process had issues. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3746116957"] = "Pandoc is not available on the system or the process had issues." --- Pandoc was not installed successfully, because the archive type is unknown. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T3962211670"] = "Pandoc was not installed successfully, because the archive type is unknown." +-- AI Studio couldn't install Pandoc because the executable was not found in the archive. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T403983772"] = "AI Studio couldn't install Pandoc because the executable was not found in the archive." --- It seems that Pandoc is not installed. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T567205144"] = "It seems that Pandoc is not installed." +-- AI Studio couldn't find the latest Pandoc version and will install version {0} instead. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T695293525"] = "AI Studio couldn't find the latest Pandoc version and will install version {0} instead." --- The latest Pandoc version was not found, installing version {0} instead. -UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T726914939"] = "The latest Pandoc version was not found, installing version {0} instead." +-- AI Studio couldn't install Pandoc. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOC::T932858631"] = "AI Studio couldn't install Pandoc." -- Pandoc is required for Microsoft Word export. UI_TEXT_CONTENT["AISTUDIO::TOOLS::PANDOCEXPORT::T1473115556"] = "Pandoc is required for Microsoft Word export." diff --git a/app/MindWork AI Studio/Tools/Pandoc.cs b/app/MindWork AI Studio/Tools/Pandoc.cs index c5826eaa..8767b1ee 100644 --- a/app/MindWork AI Studio/Tools/Pandoc.cs +++ b/app/MindWork AI Studio/Tools/Pandoc.cs @@ -35,12 +35,13 @@ public static partial class Pandoc private static bool HAS_LOGGED_AVAILABILITY_CHECK_ONCE; private static readonly HttpClient WEB_CLIENT = new(); + private static readonly SemaphoreSlim INSTALLATION_LOCK = new(1, 1); /// <summary> /// Prepares a Pandoc process by using the Pandoc process builder. /// </summary> /// <returns>The Pandoc process builder with default settings.</returns> - public static PandocProcessBuilder PreparePandocProcess() => PandocProcessBuilder.Create(); + private static PandocProcessBuilder PreparePandocProcess() => PandocProcessBuilder.Create(); /// <summary> /// Checks if pandoc is available on the system and can be started as a process or is present in AI Studio's data dir. @@ -145,12 +146,12 @@ public static partial class Pandoc catch (Exception e) { if (showMessages) - await MessageBus.INSTANCE.SendError(new(@Icons.Material.Filled.AppsOutage, TB("It seems that Pandoc is not installed."))); + await MessageBus.INSTANCE.SendError(new(@Icons.Material.Filled.AppsOutage, TB("Pandoc doesn't seem to be installed."))); if(shouldLog) LOG.LogError(e, "Pandoc availability check failed. This usually means Pandoc is not installed or not in the system PATH."); - return new(false, TB("It seems that Pandoc is not installed."), false, string.Empty, false); + return new(false, TB("Pandoc doesn't seem to be installed."), false, string.Empty, false); } finally { @@ -165,76 +166,230 @@ public static partial class Pandoc /// <returns>None</returns> public static async Task InstallAsync(RustService rustService) { + await INSTALLATION_LOCK.WaitAsync(); + var latestVersion = await FetchLatestVersionAsync(); var installDir = await GetPandocDataFolder(rustService); - ClearFolder(installDir); + var installParentDir = Path.GetDirectoryName(installDir) ?? Path.GetTempPath(); + var stagingDir = Path.Combine(installParentDir, $"pandoc-install-{Guid.NewGuid():N}"); + var pandocTempDownloadFile = Path.GetTempFileName(); LOG.LogInformation("Trying to install Pandoc v{0} to '{1}'...", latestVersion, installDir); try { - if (!Directory.Exists(installDir)) - Directory.CreateDirectory(installDir); - - // Create a temporary file to download the archive to: - var pandocTempDownloadFile = Path.GetTempFileName(); + if (!Directory.Exists(installParentDir)) + Directory.CreateDirectory(installParentDir); // // Download the latest Pandoc archive from GitHub: // - var uri = await GenerateArchiveUriAsync(); - var response = await WEB_CLIENT.GetAsync(uri); + var uri = GenerateArchiveUri(latestVersion); + if (string.IsNullOrWhiteSpace(uri)) + { + await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, TB("AI Studio couldn't install Pandoc because the archive type is unknown."))); + LOG.LogError("Pandoc was not installed, no archive is available for architecture '{Architecture}'.", CPU_ARCHITECTURE.ToUserFriendlyName()); + return; + } + + using var response = await WEB_CLIENT.GetAsync(uri); if (!response.IsSuccessStatusCode) { - await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("Pandoc was not installed successfully, because the archive was not found."))); + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("AI Studio couldn't install Pandoc because the archive was not found."))); LOG.LogError("Pandoc was not installed successfully, because the archive was not found (status code {0}): url='{1}', message='{2}'", response.StatusCode, uri, response.RequestMessage); return; } // Download the archive to the temporary file: - await using var tempFileStream = File.Create(pandocTempDownloadFile); - await response.Content.CopyToAsync(tempFileStream); + await using (var tempFileStream = File.Create(pandocTempDownloadFile)) + { + await response.Content.CopyToAsync(tempFileStream); + await tempFileStream.FlushAsync(); + } + Directory.CreateDirectory(stagingDir); if (uri.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) { - ZipFile.ExtractToDirectory(pandocTempDownloadFile, installDir); + await RunWithRetriesAsync( + () => + { + ZipFile.ExtractToDirectory(pandocTempDownloadFile, stagingDir, true); + return Task.CompletedTask; + }, + "extracting the Pandoc ZIP archive"); } else if (uri.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase)) { - await using var tgzStream = File.Open(pandocTempDownloadFile, FileMode.Open, FileAccess.Read, FileShare.Read); - await using var uncompressedStream = new GZipStream(tgzStream, CompressionMode.Decompress); - await TarFile.ExtractToDirectoryAsync(uncompressedStream, installDir, true); + await RunWithRetriesAsync( + async () => + { + await using var tgzStream = File.Open(pandocTempDownloadFile, FileMode.Open, FileAccess.Read, FileShare.Read); + await using var uncompressedStream = new GZipStream(tgzStream, CompressionMode.Decompress); + await TarFile.ExtractToDirectoryAsync(uncompressedStream, stagingDir, true); + }, + "extracting the Pandoc TAR archive"); } else { - await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, TB("Pandoc was not installed successfully, because the archive type is unknown."))); + await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, TB("AI Studio couldn't install Pandoc because the archive type is unknown."))); LOG.LogError("Pandoc was not installed, the archive is unknown: url='{0}'", uri); return; } - File.Delete(pandocTempDownloadFile); - + var stagedPandocExecutable = FindExecutableInDirectory(stagingDir, PandocProcessBuilder.PandocExecutableName); + if (string.IsNullOrWhiteSpace(stagedPandocExecutable)) + { + await MessageBus.INSTANCE.SendError(new (Icons.Material.Filled.Error, TB("AI Studio couldn't install Pandoc because the executable was not found in the archive."))); + LOG.LogError("Pandoc was not installed, the executable was not found in the extracted archive: '{StagingDir}'.", stagingDir); + return; + } + + LOG.LogInformation("Found Pandoc executable in downloaded archive: '{Executable}'.", stagedPandocExecutable); + + await ReplaceInstallationDirectoryAsync(stagingDir, installDir); await MessageBus.INSTANCE.SendSuccess(new(Icons.Material.Filled.CheckCircle, string.Format(TB("Pandoc v{0} was installed successfully."), latestVersion))); LOG.LogInformation("Pandoc v{0} was installed successfully.", latestVersion); } catch (Exception ex) { + await MessageBus.INSTANCE.SendError(new(Icons.Material.Filled.Error, TB("AI Studio couldn't install Pandoc."))); LOG.LogError(ex, "An error occurred while installing Pandoc."); } + finally + { + TryDeleteFile(pandocTempDownloadFile); + + if (Directory.Exists(stagingDir)) + await TryDeleteFolderAsync(stagingDir); + + INSTALLATION_LOCK.Release(); + } } - private static void ClearFolder(string path) + private static async Task ReplaceInstallationDirectoryAsync(string stagingDir, string installDir) { - if (!Directory.Exists(path)) - return; - + var backupDir = $"{installDir}.backup-{Guid.NewGuid():N}"; + var hasBackup = false; + var stagingWasMoved = false; + try { - Directory.Delete(path, true); + if (Directory.Exists(installDir)) + { + await MoveDirectoryWithRetriesAsync(installDir, backupDir, "moving the previous Pandoc installation to backup"); + hasBackup = true; + } + + await MoveDirectoryWithRetriesAsync(stagingDir, installDir, "moving the new Pandoc installation into place"); + stagingWasMoved = true; } catch (Exception ex) { - LOG.LogError(ex, "Error clearing pandoc installation directory."); + if (hasBackup && !stagingWasMoved && !Directory.Exists(installDir) && Directory.Exists(backupDir)) + { + try + { + await MoveDirectoryWithRetriesAsync(backupDir, installDir, "restoring the previous Pandoc installation"); + hasBackup = false; + } + catch (Exception rollbackEx) + { + LOG.LogError(rollbackEx, "Error restoring previous Pandoc installation directory. Keeping backup directory at: '{BackupDir}'.", backupDir); + } + } + + LOG.LogError(ex, "Error replacing pandoc installation directory."); + throw; + } + finally + { + if (hasBackup && stagingWasMoved && Directory.Exists(backupDir)) + await TryDeleteFolderAsync(backupDir); + } + } + + private static string FindExecutableInDirectory(string rootDirectory, string executableName) + { + if (!Directory.Exists(rootDirectory)) + return string.Empty; + + var rootExecutablePath = Path.Combine(rootDirectory, executableName); + if (File.Exists(rootExecutablePath)) + return rootExecutablePath; + + foreach (var subdirectory in Directory.GetDirectories(rootDirectory, "*", SearchOption.AllDirectories)) + { + var pandocPath = Path.Combine(subdirectory, executableName); + if (File.Exists(pandocPath)) + return pandocPath; + } + + return string.Empty; + } + + private static async Task MoveDirectoryWithRetriesAsync(string sourceDir, string destinationDir, string operationName) + { + await RunWithRetriesAsync( + () => + { + Directory.Move(sourceDir, destinationDir); + return Task.CompletedTask; + }, + operationName, + maxAttempts: 8); + } + + private static async Task RunWithRetriesAsync(Func<Task> operation, string operationName, int maxAttempts = 4) + { + for (var attempt = 1; attempt <= maxAttempts; attempt++) + { + try + { + await operation(); + return; + } + catch (Exception ex) when (attempt < maxAttempts && ex is IOException or UnauthorizedAccessException) + { + LOG.LogWarning(ex, "Error while {OperationName}; retrying attempt {Attempt}/{MaxAttempts}.", operationName, attempt + 1, maxAttempts); + await Task.Delay(TimeSpan.FromMilliseconds(250 * attempt)); + } + } + } + + private static void TryDeleteFile(string path) + { + if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) + return; + + try + { + File.Delete(path); + } + catch (Exception ex) + { + LOG.LogWarning(ex, "Was not able to delete temporary Pandoc archive: '{Path}'.", path); + } + } + + private static async Task TryDeleteFolderAsync(string path) + { + if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path)) + return; + + try + { + await RunWithRetriesAsync( + () => + { + Directory.Delete(path, true); + return Task.CompletedTask; + }, + $"deleting temporary Pandoc directory '{path}'", + maxAttempts: 3); + } + catch (Exception ex) + { + LOG.LogWarning(ex, "Was not able to delete temporary Pandoc directory: '{Path}'.", path); } } @@ -248,7 +403,7 @@ public static partial class Pandoc if (!response.IsSuccessStatusCode) { LOG.LogError("Code {StatusCode}: Could not fetch Pandoc's latest page: {Response}", response.StatusCode, response.RequestMessage); - await MessageBus.INSTANCE.SendWarning(new (Icons.Material.Filled.Warning, string.Format(TB("The latest Pandoc version was not found, installing version {0} instead."), FALLBACK_VERSION.ToString()))); + await MessageBus.INSTANCE.SendWarning(new (Icons.Material.Filled.Warning, string.Format(TB("AI Studio couldn't find the latest Pandoc version and will install version {0} instead."), FALLBACK_VERSION.ToString()))); return FALLBACK_VERSION.ToString(); } @@ -257,7 +412,7 @@ public static partial class Pandoc if (!versionMatch.Success) { LOG.LogError("The latest version regex returned nothing: {0}", versionMatch.Groups.ToString()); - await MessageBus.INSTANCE.SendWarning(new (Icons.Material.Filled.Warning, string.Format(TB("The latest Pandoc version was not found, installing version {0} instead."), FALLBACK_VERSION.ToString()))); + await MessageBus.INSTANCE.SendWarning(new (Icons.Material.Filled.Warning, string.Format(TB("AI Studio couldn't find the latest Pandoc version and will install version {0} instead."), FALLBACK_VERSION.ToString()))); return FALLBACK_VERSION.ToString(); } @@ -272,6 +427,11 @@ public static partial class Pandoc public static async Task<string> GenerateArchiveUriAsync() { var version = await FetchLatestVersionAsync(); + return GenerateArchiveUri(version); + } + + private static string GenerateArchiveUri(string version) + { var baseUri = $"{DOWNLOAD_URL}/{version}/pandoc-{version}-"; return CPU_ARCHITECTURE switch { diff --git a/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs b/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs index 6d95ad9f..6d0909f8 100644 --- a/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs +++ b/app/MindWork AI Studio/Tools/PandocProcessBuilder.cs @@ -220,6 +220,17 @@ public sealed class PandocProcessBuilder } } + foreach (var candidate in SystemPandocExecutableCandidates(PandocExecutableName)) + { + if (!File.Exists(candidate)) + continue; + + if (shouldLog) + LOGGER.LogInformation("Found system Pandoc installation at: '{Path}'.", candidate); + + return new(candidate, false); + } + // // When no local installation was found, we assume that the pandoc executable is in the system PATH: // @@ -238,4 +249,59 @@ public sealed class PandocProcessBuilder /// Reads the os platform to determine the used executable name. /// </summary> public static string PandocExecutableName => CPU_ARCHITECTURE is RID.WIN_ARM64 or RID.WIN_X64 ? "pandoc.exe" : "pandoc"; + + private static IEnumerable<string> SystemPandocExecutableCandidates(string executableName) + { + var candidates = new List<string>(); + + switch (CPU_ARCHITECTURE) + { + case RID.WIN_X64 or RID.WIN_ARM64: + AddCandidate(candidates, Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Pandoc", executableName); + AddCandidate(candidates, Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Pandoc", executableName); + AddCandidate(candidates, Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "Pandoc", executableName); + break; + + case RID.OSX_X64 or RID.OSX_ARM64: + AddCandidate(candidates, "/opt/homebrew/bin", executableName); + AddCandidate(candidates, "/usr/local/bin", executableName); + AddCandidate(candidates, "/usr/bin", executableName); + break; + + case RID.LINUX_X64 or RID.LINUX_ARM64: + AddCandidate(candidates, "/usr/local/bin", executableName); + AddCandidate(candidates, "/usr/bin", executableName); + AddCandidate(candidates, "/snap/bin", executableName); + + var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); + AddCandidate(candidates, homeDirectory, ".local", "bin", executableName); + break; + } + + foreach (var pathDirectory in GetPathDirectories()) + AddCandidate(candidates, pathDirectory, executableName); + + var comparer = CPU_ARCHITECTURE is RID.WIN_X64 or RID.WIN_ARM64 + ? StringComparer.OrdinalIgnoreCase + : StringComparer.Ordinal; + return candidates.Distinct(comparer); + } + + private static IEnumerable<string> GetPathDirectories() + { + var pathValue = Environment.GetEnvironmentVariable("PATH"); + if (string.IsNullOrWhiteSpace(pathValue)) + yield break; + + foreach (var pathDirectory in pathValue.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + yield return pathDirectory; + } + + private static void AddCandidate(List<string> candidates, params string[] pathParts) + { + if (pathParts.Any(string.IsNullOrWhiteSpace)) + return; + + candidates.Add(Path.Combine(pathParts)); + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md index af2ad840..2fa98028 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md @@ -1,5 +1,7 @@ # v26.5.5, build 240 (2026-05-xx xx:xx UTC) - Improved the app's security foundation with major modernization of the native runtime and its internal communication layer. This work is mostly invisible during everyday use, but it replaces older components that no longer received the security updates we require. We also continued updating security-sensitive dependencies so AI Studio stays on a healthier, better maintained base. +- Improved the Pandoc management and detection process to make it more reliable. +- Fixed the Pandoc installation, which could fail and prevent AI Studio from installing its local Pandoc dependency. - Upgraded the native secret storage integration to `keyring-core`, keeping API keys in the secure credential store provided by the operating system. - Upgraded Rust to v1.95.0. - Upgraded .NET to v9.0.16. diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index 1abd7951..f9ec3dbb 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -889,7 +889,7 @@ pub async fn resume_shortcuts(_token: APIToken) -> Json<ShortcutResponse> { continue; } - match register_shortcut_with_callback(&app_handle, shortcut, *shortcut_id, event_sender.clone()) { + match register_shortcut_with_callback(app_handle, shortcut, *shortcut_id, event_sender.clone()) { Ok(_) => { info!(Source = "Tauri"; "Re-registered shortcut '{shortcut}' for '{}'.", shortcut_id); success_count += 1; diff --git a/runtime/src/pandoc.rs b/runtime/src/pandoc.rs index f2dc6a8f..4b6f91f4 100644 --- a/runtime/src/pandoc.rs +++ b/runtime/src/pandoc.rs @@ -1,13 +1,16 @@ -use std::path::{Path, PathBuf}; +use std::collections::HashSet; +use std::env; use std::fs; +use std::path::{Path, PathBuf}; use std::sync::OnceLock; -use log::warn; +use log::{info, warn}; use tokio::process::Command; use crate::environment::DATA_DIRECTORY; use crate::metadata::META_DATA; /// Tracks whether the RID mismatch warning has been logged. static HAS_LOGGED_RID_MISMATCH: OnceLock<()> = OnceLock::new(); +static HAS_LOGGED_PANDOC_PATH: OnceLock<()> = OnceLock::new(); pub struct PandocExecutable { pub executable: String, @@ -114,28 +117,43 @@ impl PandocProcessBuilder { // Any local installation should be preferred over the system-wide installation. let data_folder = PathBuf::from(DATA_DIRECTORY.get().unwrap()); let local_installation_root_directory = data_folder.join("pandoc"); + let executable_name = Self::pandoc_executable_name(); if local_installation_root_directory.exists() { - let executable_name = Self::pandoc_executable_name(); + if let Ok(pandoc_path) = Self::find_executable_in_dir(&local_installation_root_directory, &executable_name) { + HAS_LOGGED_PANDOC_PATH.get_or_init(|| { + info!(Source = "PandocProcessBuilder"; "Found local Pandoc installation at: '{}'.", pandoc_path.to_string_lossy() + ); + }); - if let Ok(entries) = fs::read_dir(&local_installation_root_directory) { - for entry in entries.flatten() { - let path = entry.path(); - if path.is_dir() { - if let Ok(pandoc_path) = Self::find_executable_in_dir(&path, &executable_name) { - return PandocExecutable { - executable: pandoc_path.to_string_lossy().to_string(), - is_local_installation: true, - }; - } - } - } + return PandocExecutable { + executable: pandoc_path.to_string_lossy().to_string(), + is_local_installation: true, + }; + } + } + + for candidate in Self::system_pandoc_executable_candidates(&executable_name) { + if candidate.exists() && candidate.is_file() { + HAS_LOGGED_PANDOC_PATH.get_or_init(|| { + info!(Source = "PandocProcessBuilder"; "Found system Pandoc installation at: '{}'.", candidate.to_string_lossy() + ); + }); + + return PandocExecutable { + executable: candidate.to_string_lossy().to_string(), + is_local_installation: false, + }; } } // When no local installation was found, we assume that the pandoc executable is in the system PATH: + HAS_LOGGED_PANDOC_PATH.get_or_init(|| { + warn!(Source = "PandocProcessBuilder"; "Falling back to system PATH for the Pandoc executable: '{}'.", executable_name); + }); + PandocExecutable { - executable: Self::pandoc_executable_name(), + executable: executable_name, is_local_installation: false, } } @@ -161,6 +179,56 @@ impl PandocProcessBuilder { Err("Executable not found".into()) } + fn system_pandoc_executable_candidates(executable_name: &str) -> Vec<PathBuf> { + let mut candidates: Vec<PathBuf> = Vec::new(); + match env::consts::OS { + "windows" => { + Self::push_env_candidate(&mut candidates, "LOCALAPPDATA", &["Pandoc", executable_name]); + Self::push_env_candidate(&mut candidates, "ProgramFiles", &["Pandoc", executable_name]); + Self::push_env_candidate(&mut candidates, "ProgramFiles(x86)", &["Pandoc", executable_name]); + }, + "macos" => { + candidates.push(PathBuf::from("/opt/homebrew/bin").join(executable_name)); + candidates.push(PathBuf::from("/usr/local/bin").join(executable_name)); + candidates.push(PathBuf::from("/usr/bin").join(executable_name)); + }, + "linux" => { + candidates.push(PathBuf::from("/usr/local/bin").join(executable_name)); + candidates.push(PathBuf::from("/usr/bin").join(executable_name)); + candidates.push(PathBuf::from("/snap/bin").join(executable_name)); + + if let Some(home_dir) = env::var_os("HOME") { + candidates.push(PathBuf::from(home_dir).join(".local").join("bin").join(executable_name)); + } + }, + _ => {}, + } + + if let Some(path_value) = env::var_os("PATH") { + for path_dir in env::split_paths(&path_value) { + candidates.push(path_dir.join(executable_name)); + } + } + + let mut seen = HashSet::new(); + candidates + .into_iter() + .filter(|path| seen.insert(path.clone())) + .collect() + } + + fn push_env_candidate(candidates: &mut Vec<PathBuf>, env_name: &str, parts: &[&str]) { + if let Some(root) = env::var_os(env_name) { + let mut path = PathBuf::from(root); + + for part in parts { + path.push(part); + } + + candidates.push(path); + } + } + /// Determines the executable name based on the current OS at runtime. /// /// This uses runtime detection instead of metadata to ensure correct behavior From 9419c4ed44c6a3b88c6ec3204137811cc6803135 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Sat, 16 May 2026 18:53:53 +0200 Subject: [PATCH 16/21] Improved Rust syntax by Clippy suggestions (#765) --- runtime/src/app_window.rs | 22 ++++----- runtime/src/environment.rs | 13 ++--- runtime/src/pandoc.rs | 71 +++++++++++++--------------- runtime/src/qdrant.rs | 10 ++-- runtime/src/stale_process_cleanup.rs | 2 +- 5 files changed, 51 insertions(+), 67 deletions(-) diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index f9ec3dbb..b52be5a5 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -245,10 +245,8 @@ fn should_open_in_system_browser<R: tauri::Runtime>(webview: &tauri::Webview<R>, } } - if let Ok(current_url) = webview.url() { - if same_origin(¤t_url, url) { - return false; - } + if let Ok(current_url) = webview.url() && same_origin(¤t_url, url) { + return false; } !is_local_host(url.host_str()) @@ -415,10 +413,8 @@ pub async fn change_location_to(url: &str) { } } - if let Ok(parsed_url) = tauri::Url::parse(url) { - if is_local_http_url(&parsed_url) { - *APPROVED_APP_URL.lock().unwrap() = Some(parsed_url); - } + if let Ok(parsed_url) = tauri::Url::parse(url) && is_local_http_url(&parsed_url) { + *APPROVED_APP_URL.lock().unwrap() = Some(parsed_url); } let js_location_change = format!("window.location = '{url}';"); @@ -685,12 +681,10 @@ pub async fn register_shortcut(_token: APIToken, payload: Json<RegisterShortcutR let mut registered_shortcuts = REGISTERED_SHORTCUTS.lock().unwrap(); // Unregister the old shortcut if one exists for this name: - if let Some(old_shortcut) = registered_shortcuts.get(&id) { - if !old_shortcut.is_empty() { - match shortcut_manager.unregister(old_shortcut.as_str()) { - Ok(_) => info!(Source = "Tauri"; "Unregistered old shortcut '{old_shortcut}' for '{}'.", id), - Err(error) => warn!(Source = "Tauri"; "Failed to unregister old shortcut '{old_shortcut}': {error}"), - } + if let Some(old_shortcut) = registered_shortcuts.get(&id) && !old_shortcut.is_empty() { + match shortcut_manager.unregister(old_shortcut.as_str()) { + Ok(_) => info!(Source = "Tauri"; "Unregistered old shortcut '{old_shortcut}' for '{}'.", id), + Err(error) => warn!(Source = "Tauri"; "Failed to unregister old shortcut '{old_shortcut}': {error}"), } } diff --git a/runtime/src/environment.rs b/runtime/src/environment.rs index 68198fbd..1e45b5f3 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -87,10 +87,8 @@ fn normalize_locale_tag(locale: &str) -> Option<String> { return None; } - if let Some(region) = segments.next() { - if region.len() == 2 && region.chars().all(|c| c.is_ascii_alphabetic()) { - return Some(format!("{}-{}", language, region.to_ascii_uppercase())); - } + if let Some(region) = segments.next() && region.len() == 2 && region.chars().all(|c| c.is_ascii_alphabetic()) { + return Some(format!("{}-{}", language, region.to_ascii_uppercase())); } Some(language) @@ -418,10 +416,9 @@ fn load_policy_values_from_directories(directories: &[PathBuf]) -> HashMap<Strin } let secret_path = directory.join(ENTERPRISE_POLICY_SECRET_FILE_NAME); - if let Some(secret_values) = read_policy_yaml_mapping(&secret_path) { - if let Some(secret) = secret_values.get("config_encryption_secret") { - insert_first_non_empty_value(&mut values, "config_encryption_secret", secret); - } + if let Some(secret_values) = read_policy_yaml_mapping(&secret_path) + && let Some(secret) = secret_values.get("config_encryption_secret") { + insert_first_non_empty_value(&mut values, "config_encryption_secret", secret); } } diff --git a/runtime/src/pandoc.rs b/runtime/src/pandoc.rs index 4b6f91f4..82270059 100644 --- a/runtime/src/pandoc.rs +++ b/runtime/src/pandoc.rs @@ -119,18 +119,17 @@ impl PandocProcessBuilder { let local_installation_root_directory = data_folder.join("pandoc"); let executable_name = Self::pandoc_executable_name(); - if local_installation_root_directory.exists() { - if let Ok(pandoc_path) = Self::find_executable_in_dir(&local_installation_root_directory, &executable_name) { - HAS_LOGGED_PANDOC_PATH.get_or_init(|| { - info!(Source = "PandocProcessBuilder"; "Found local Pandoc installation at: '{}'.", pandoc_path.to_string_lossy() - ); - }); + if local_installation_root_directory.exists() + && let Ok(pandoc_path) = Self::find_executable_in_dir(&local_installation_root_directory, &executable_name) { + HAS_LOGGED_PANDOC_PATH.get_or_init(|| { + info!(Source = "PandocProcessBuilder"; "Found local Pandoc installation at: '{}'.", pandoc_path.to_string_lossy() + ); + }); - return PandocExecutable { - executable: pandoc_path.to_string_lossy().to_string(), - is_local_installation: true, - }; - } + return PandocExecutable { + executable: pandoc_path.to_string_lossy().to_string(), + is_local_installation: true, + }; } for candidate in Self::system_pandoc_executable_candidates(&executable_name) { @@ -168,10 +167,8 @@ impl PandocProcessBuilder { if let Ok(entries) = fs::read_dir(dir) { for entry in entries.flatten() { let path = entry.path(); - if path.is_dir() { - if let Ok(found_path) = Self::find_executable_in_dir(&path, executable_name) { - return Ok(found_path); - } + if path.is_dir() && let Ok(found_path) = Self::find_executable_in_dir(&path, executable_name) { + return Ok(found_path); } } } @@ -240,33 +237,31 @@ impl PandocProcessBuilder { let runtime_os = std::env::consts::OS; let runtime_arch = std::env::consts::ARCH; - if let Ok(metadata) = META_DATA.lock() { - if let Some(metadata) = metadata.as_ref() { - let metadata_arch = &metadata.architecture; + if let Ok(metadata) = META_DATA.lock() && let Some(metadata) = metadata.as_ref() { + let metadata_arch = &metadata.architecture; - // Determine expected OS from metadata: - let metadata_is_windows = metadata_arch.starts_with("win-"); - let metadata_is_macos = metadata_arch.starts_with("osx-"); - let metadata_is_linux = metadata_arch.starts_with("linux-"); + // Determine expected OS from metadata: + let metadata_is_windows = metadata_arch.starts_with("win-"); + let metadata_is_macos = metadata_arch.starts_with("osx-"); + let metadata_is_linux = metadata_arch.starts_with("linux-"); - // Compare with runtime OS: - let runtime_is_windows = runtime_os == "windows"; - let runtime_is_macos = runtime_os == "macos"; - let runtime_is_linux = runtime_os == "linux"; + // Compare with runtime OS: + let runtime_is_windows = runtime_os == "windows"; + let runtime_is_macos = runtime_os == "macos"; + let runtime_is_linux = runtime_os == "linux"; - let os_mismatch = (metadata_is_windows != runtime_is_windows) - || (metadata_is_macos != runtime_is_macos) - || (metadata_is_linux != runtime_is_linux); + let os_mismatch = (metadata_is_windows != runtime_is_windows) + || (metadata_is_macos != runtime_is_macos) + || (metadata_is_linux != runtime_is_linux); - if os_mismatch { - warn!( - Source = "Pandoc"; - "Runtime-detected OS '{}-{}' differs from metadata architecture '{}'. Using runtime-detected OS. This is expected on dev machines where metadata.txt may be outdated.", - runtime_os, - runtime_arch, - metadata_arch - ); - } + if os_mismatch { + warn!( + Source = "Pandoc"; + "Runtime-detected OS '{}-{}' differs from metadata architecture '{}'. Using runtime-detected OS. This is expected on dev machines where metadata.txt may be outdated.", + runtime_os, + runtime_arch, + metadata_arch + ); } } }); diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index c24b7d6d..2ec9d1e9 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -100,12 +100,10 @@ pub async fn qdrant_port(_token: APIToken) -> Json<ProvideQdrantInfo> { /// Starts the Qdrant server in a separate process. pub fn start_qdrant_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>){ let path = qdrant_base_path(); - if !path.exists() { - if let Err(e) = fs::create_dir_all(&path){ - error!(Source="Qdrant"; "The required directory to host the Qdrant database could not be created: {}", e); - set_qdrant_unavailable(format!("The Qdrant data directory could not be created: {e}")); - return; - }; + if !path.exists() && let Err(e) = fs::create_dir_all(&path){ + error!(Source="Qdrant"; "The required directory to host the Qdrant database could not be created: {}", e); + set_qdrant_unavailable(format!("The Qdrant data directory could not be created: {e}")); + return; } let (cert_path, key_path) = match create_temp_tls_files(&path) { diff --git a/runtime/src/stale_process_cleanup.rs b/runtime/src/stale_process_cleanup.rs index 7d177ac8..73e92111 100644 --- a/runtime/src/stale_process_cleanup.rs +++ b/runtime/src/stale_process_cleanup.rs @@ -50,7 +50,7 @@ pub fn kill_stale_process(pid_file_path: PathBuf, sidecar_type: SidecarType) -> let killed = process.kill_with(Signal::Kill).unwrap_or_else(|| process.kill()); if !killed { - return Err(Error::new(ErrorKind::Other, "Failed to kill process")); + return Err(Error::other("Failed to kill process")); } info!(Source="Stale Process Cleanup";"{}: Killed process: \"{}\"", sidecar_type,pid_file_path.display()); } else { From 378aaaa368fcfe97d4a9b8e257854ccccd37b3e6 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Sat, 16 May 2026 19:13:27 +0200 Subject: [PATCH 17/21] Released the transcription feature (#766) (#766) --- app/MindWork AI Studio/Assistants/I18N/allTexts.lua | 4 ++-- .../Components/Settings/SettingsPanelTranscription.razor | 1 - app/MindWork AI Studio/Plugins/configuration/plugin.lua | 4 ++-- .../de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua | 4 ++-- .../en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua | 4 ++-- .../Settings/DataModel/PreviewFeaturesExtensions.cs | 3 ++- .../Settings/DataModel/PreviewVisibilityExtensions.cs | 1 - .../Tools/Services/GlobalShortcutService.cs | 4 +--- app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md | 1 + 9 files changed, 12 insertions(+), 14 deletions(-) diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index 73b7b83b..a028108e 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -6676,8 +6676,8 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2708 -- Unknown preview feature UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2722827307"] = "Unknown preview feature" --- Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text -UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T714355911"] = "Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text" +-- Transcription: Convert recordings and audio files into text +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T4247148645"] = "Transcription: Convert recordings and audio files into text" -- Use no data sources, when sending an assistant result to a chat UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::SENDTOCHATDATASOURCEBEHAVIOREXTENSIONS::T1223925477"] = "Use no data sources, when sending an assistant result to a chat" diff --git a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor index 7b417e58..d99a2e14 100644 --- a/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor +++ b/app/MindWork AI Studio/Components/Settings/SettingsPanelTranscription.razor @@ -5,7 +5,6 @@ @if (PreviewFeatures.PRE_SPEECH_TO_TEXT_2026.IsEnabled(this.SettingsManager)) { <ExpansionPanel HeaderIcon="@Icons.Material.Filled.VoiceChat" HeaderText="@T("Configure Transcription Providers")"> - <PreviewBeta ApplyInnerScrollingFix="true"/> <MudText Typo="Typo.h4" Class="mb-3"> @T("Configured Transcription Providers") </MudText> diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index 6cd5858d..b4a942a7 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -173,8 +173,8 @@ CONFIG["SETTINGS"] = {} -- Configure the enabled preview features: -- Allowed values are can be found in https://github.com/MindWorkAI/AI-Studio/app/MindWork%20AI%20Studio/Settings/DataModel/PreviewFeatures.cs --- Examples are PRE_WRITER_MODE_2024, PRE_RAG_2024, PRE_SPEECH_TO_TEXT_2026. --- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024", "PRE_SPEECH_TO_TEXT_2026" } +-- Examples are PRE_WRITER_MODE_2024 and PRE_RAG_2024. +-- CONFIG["SETTINGS"]["DataApp.EnabledPreviewFeatures"] = { "PRE_RAG_2024" } -- Configure the preselected provider. -- It must be one of the provider IDs defined in CONFIG["LLM_PROVIDERS"]. diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 32598b6f..300ad595 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -6678,8 +6678,8 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2708 -- Unknown preview feature UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2722827307"] = "Unbekannte Vorschau-Funktion" --- Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text -UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T714355911"] = "Transkription: Vorschau unseres Sprache-zu-Text-Systems, mit dem Sie Aufnahmen und Audiodateien in Text transkribieren können" +-- Transcription: Convert recordings and audio files into text +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T4247148645"] = "Transkription: Aufnahmen und Audiodateien in Text umwandeln" -- Use no data sources, when sending an assistant result to a chat UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::SENDTOCHATDATASOURCEBEHAVIOREXTENSIONS::T1223925477"] = "Keine Datenquellen vorauswählen, wenn ein Ergebnis von einem Assistenten an einen neuen Chat gesendet wird" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index c837f96d..e629d833 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -6678,8 +6678,8 @@ UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2708 -- Unknown preview feature UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T2722827307"] = "Unknown preview feature" --- Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text -UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T714355911"] = "Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text" +-- Transcription: Convert recordings and audio files into text +UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::PREVIEWFEATURESEXTENSIONS::T4247148645"] = "Transcription: Convert recordings and audio files into text" -- Use no data sources, when sending an assistant result to a chat UI_TEXT_CONTENT["AISTUDIO::SETTINGS::DATAMODEL::SENDTOCHATDATASOURCEBEHAVIOREXTENSIONS::T1223925477"] = "Use no data sources, when sending an assistant result to a chat" diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs index fa10ecad..8fdc8d4e 100644 --- a/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/PreviewFeaturesExtensions.cs @@ -14,7 +14,7 @@ public static class PreviewFeaturesExtensions PreviewFeatures.PRE_PLUGINS_2025 => TB("Plugins: Preview of our plugin system where you can extend the functionality of the app"), PreviewFeatures.PRE_READ_PDF_2025 => TB("Read PDF: Preview of our PDF reading system where you can read and extract text from PDF files"), PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => TB("Document Analysis: Preview of our document analysis system where you can analyze and extract information from documents"), - PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => TB("Transcription: Preview of our speech to text system where you can transcribe recordings and audio files into text"), + PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => TB("Transcription: Convert recordings and audio files into text"), _ => TB("Unknown preview feature") }; @@ -33,6 +33,7 @@ public static class PreviewFeaturesExtensions PreviewFeatures.PRE_READ_PDF_2025 => true, PreviewFeatures.PRE_PLUGINS_2025 => true, PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025 => true, + PreviewFeatures.PRE_SPEECH_TO_TEXT_2026 => true, _ => false }; diff --git a/app/MindWork AI Studio/Settings/DataModel/PreviewVisibilityExtensions.cs b/app/MindWork AI Studio/Settings/DataModel/PreviewVisibilityExtensions.cs index 30764bfe..30a1b4ea 100644 --- a/app/MindWork AI Studio/Settings/DataModel/PreviewVisibilityExtensions.cs +++ b/app/MindWork AI Studio/Settings/DataModel/PreviewVisibilityExtensions.cs @@ -12,7 +12,6 @@ public static class PreviewVisibilityExtensions if (visibility >= PreviewVisibility.BETA) { features.Add(PreviewFeatures.PRE_DOCUMENT_ANALYSIS_2025); - features.Add(PreviewFeatures.PRE_SPEECH_TO_TEXT_2026); } if (visibility >= PreviewVisibility.ALPHA) diff --git a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs index 7d701670..9f33c68a 100644 --- a/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs +++ b/app/MindWork AI Studio/Tools/Services/GlobalShortcutService.cs @@ -185,9 +185,7 @@ public sealed class GlobalShortcutService : BackgroundService, IMessageBusReceiv return new(shortcut, isEnabled, false); var fallbackShortcut = settingsSnapshot.App.ShortcutVoiceRecording; - var fallbackEnabled = - settingsSnapshot.App.EnabledPreviewFeatures.Contains(PreviewFeatures.PRE_SPEECH_TO_TEXT_2026) && - !string.IsNullOrWhiteSpace(settingsSnapshot.App.UseTranscriptionProvider); + var fallbackEnabled = !string.IsNullOrWhiteSpace(settingsSnapshot.App.UseTranscriptionProvider); if (!fallbackEnabled || string.IsNullOrWhiteSpace(fallbackShortcut)) return new(shortcut, isEnabled, false); diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md index 2fa98028..1008fe32 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md @@ -1,4 +1,5 @@ # v26.5.5, build 240 (2026-05-xx xx:xx UTC) +- Released the voice recording and transcription for all users. You no longer need to enable a preview feature to configure transcription providers, select a transcription provider, or use dictation. - Improved the app's security foundation with major modernization of the native runtime and its internal communication layer. This work is mostly invisible during everyday use, but it replaces older components that no longer received the security updates we require. We also continued updating security-sensitive dependencies so AI Studio stays on a healthier, better maintained base. - Improved the Pandoc management and detection process to make it more reliable. - Fixed the Pandoc installation, which could fail and prevent AI Studio from installing its local Pandoc dependency. From 7a092418884223b6ae8a1fda5fe412be7cca3f8a Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Mon, 18 May 2026 16:26:51 +0200 Subject: [PATCH 18/21] Configure ERI servers in config plugins (#767) --- AGENTS.md | 5 +- app/MindWork AI Studio.sln.DotSettings | 1 + .../Assistants/I18N/allTexts.lua | 63 +++++ ...rceERIV1UsernamePasswordExportDialog.razor | 26 ++ ...ERIV1UsernamePasswordExportDialog.razor.cs | 37 +++ ...ERIV1UsernamePasswordExportDialogResult.cs | 5 + .../Dialogs/DataSourceERI_V1Dialog.razor.cs | 5 +- .../Dialogs/DataSourceERI_V1InfoDialog.razor | 2 +- .../DataSourceERI_V1InfoDialog.razor.cs | 30 ++- .../Settings/SettingsDialogDataSources.razor | 27 +- .../SettingsDialogDataSources.razor.cs | 108 +++++++- .../Layout/MainLayout.razor.cs | 2 + .../Pages/Information.razor | 2 + .../Pages/Information.razor.cs | 4 + .../Plugins/configuration/plugin.lua | 48 ++++ .../plugin.lua | 63 +++++ .../plugin.lua | 63 +++++ .../DataSourceERIUsernamePasswordMode.cs | 19 ++ .../Settings/DataModel/DataSourceERI_V1.cs | 252 +++++++++++++++++- .../DataModel/DataSourceLocalDirectory.cs | 6 + .../Settings/DataModel/DataSourceLocalFile.cs | 6 + .../Settings/IDataSource.cs | 18 +- .../Settings/IERIDataSource.cs | 6 + .../Settings/IExternalDataSource.cs | 2 +- .../Tools/ERIClient/ERIClientV1.cs | 18 +- .../PluginSystem/PendingEnterpriseApiKey.cs | 35 +-- .../PluginSystem/PendingEnterpriseApiKeys.cs | 34 +++ .../PluginSystem/PendingEnterpriseSecret.cs | 14 + .../PluginSystem/PendingEnterpriseSecrets.cs | 34 +++ .../Tools/PluginSystem/PluginConfiguration.cs | 34 +++ .../PluginSystem/PluginConfigurationObject.cs | 95 ++++++- .../PluginSystem/PluginFactory.Loading.cs | 4 + .../Tools/SecretStoreType.cs | 11 +- .../Tools/SecretStoreTypeExtensions.cs | 4 +- .../Services/EnterpriseEnvironmentService.cs | 21 +- .../Tools/Services/RustService.OS.cs | 31 +++ .../Tools/Services/RustService.Secrets.cs | 79 ++++-- .../Tools/Services/RustService.cs | 3 + .../wwwroot/changelog/v26.5.5.md | 3 + runtime/Cargo.lock | 41 ++- runtime/Cargo.toml | 1 + runtime/src/environment.rs | 10 +- runtime/src/runtime_api.rs | 1 + 43 files changed, 1165 insertions(+), 108 deletions(-) create mode 100644 app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialog.razor create mode 100644 app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialog.razor.cs create mode 100644 app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialogResult.cs create mode 100644 app/MindWork AI Studio/Settings/DataModel/DataSourceERIUsernamePasswordMode.cs create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/PendingEnterpriseApiKeys.cs create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/PendingEnterpriseSecret.cs create mode 100644 app/MindWork AI Studio/Tools/PluginSystem/PendingEnterpriseSecrets.cs diff --git a/AGENTS.md b/AGENTS.md index 7908fdcd..48a25021 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,7 @@ Currently, no automated test suite exists in the repository. Key modules: - `app_window.rs` - Tauri window management, updater integration - `dotnet.rs` - Launches and manages the .NET sidecar process -- `runtime_api.rs` - Rocket-based HTTPS API for .NET ↔ Rust communication +- `runtime_api.rs` - Axum-based HTTPS API for .NET ↔ Rust communication - `certificate.rs` - Generates self-signed TLS certificates for secure IPC - `secret.rs` - Secure secret storage using OS keyring (Keychain/Credential Manager) - `clipboard.rs` - Cross-platform clipboard operations @@ -152,7 +152,7 @@ Multi-level confidence scheme allows users to control which providers see which **Rust:** - Tauri 1.8 - Desktop application framework -- Rocket - HTTPS API server +- Axum - HTTPS API server - tokio - Async runtime - keyring - OS keyring integration - pdfium-render - PDF text extraction @@ -187,6 +187,7 @@ Multi-level confidence scheme allows users to control which providers see which - **File changes require Write/Edit tools** - Never use bash commands like `cat <<EOF` or `echo >` - **End of file formatting** - Do not append an extra empty line at the end of files. - **No automated formatting for Rust or .NET files** - Never run automated formatters on Rust files (`.rs`) or .NET files (`.cs`, `.razor`, `.csproj`, etc.). Only make the minimal manual formatting changes required for the specific edit. +- **I18N resources are generated** - Do not manually edit `app/MindWork AI Studio/Assistants/I18N/allTexts.lua`, `app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua`, or `app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua`. These files are updated automatically by the I18N process. - **Spaces in paths** - Always quote paths with spaces in bash commands - **Agent-run .NET builds** - Do not run `.NET` builds from an agent. Ask the user to run the build locally in their IDE, preferably via `cd app/Build && dotnet run build` in an IDE terminal, then wait for their feedback before continuing. - **Debug environment** - Reads `startup.env` file with IPC credentials diff --git a/app/MindWork AI Studio.sln.DotSettings b/app/MindWork AI Studio.sln.DotSettings index d35acefd..8919d73e 100644 --- a/app/MindWork AI Studio.sln.DotSettings +++ b/app/MindWork AI Studio.sln.DotSettings @@ -2,6 +2,7 @@ <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=AI/@EntryIndexedValue">AI</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EDI/@EntryIndexedValue">EDI</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERI/@EntryIndexedValue">ERI</s:String> + <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=ERIV/@EntryIndexedValue">ERIV</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=FNV/@EntryIndexedValue">FNV</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GWDG/@EntryIndexedValue">GWDG</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HF/@EntryIndexedValue">HF</s:String> diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index a028108e..a4205982 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -3631,6 +3631,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2879113658"] = -- Maximum matches per query UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2889706179"] = "Maximum matches per query" +-- Failed to read the user's username from the operating system. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2909734556"] = "Failed to read the user's username from the operating system." + -- Open web link, show more information UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2968752071"] = "Open web link, show more information" @@ -3682,6 +3685,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T742006305"] = " -- Embeddings UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T951463987"] = "Embeddings" +-- Use the same username and password for all users +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1769874785"] = "Use the same username and password for all users" + +-- Username and password mode +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1787063064"] = "Username and password mode" + +-- How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3081234668"] = "How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?" + +-- User-managed username and password +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T365340972"] = "User-managed username and password" + +-- Export +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3898821075"] = "Export" + +-- Read each user's username from the operating system and share one password +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T76405695"] = "Read each user's username from the operating system and share one password" + +-- Cancel +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T900713019"] = "Cancel" + -- Describe what data this directory contains to help the AI select it. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T1136409150"] = "Describe what data this directory contains to help the AI select it." @@ -4810,6 +4834,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T145419 -- Delete UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1469573738"] = "Delete" +-- Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1577531115"] = "Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin." + +-- Cannot export this ERI data source because the authentication secret could not be encrypted. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1592527757"] = "Cannot export this ERI data source because the authentication secret could not be encrypted." + -- External (ERI) UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1652430727"] = "External (ERI)" @@ -4840,6 +4870,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T269820 -- Embedding UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T2838542994"] = "Embedding" +-- This data source is managed by your organization. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3031462878"] = "This data source is managed by your organization." + -- Edit UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3267849393"] = "Edit" @@ -4864,21 +4897,39 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T352566 -- No data sources configured yet. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3549650120"] = "No data sources configured yet." +-- Export Access Token? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3595669127"] = "Export Access Token?" + +-- Export ERI Data Source +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3831281036"] = "Export ERI Data Source" + -- Actions UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3865031940"] = "Actions" +-- This ERI data source has an access token configured. Do you want to include the encrypted access token in the export? Note: The recipient will need the same encryption secret to use the access token. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T4027572258"] = "This ERI data source has an access token configured. Do you want to include the encrypted access token in the export? Note: The recipient will need the same encryption secret to use the access token." + -- Configured Data Sources UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T543942217"] = "Configured Data Sources" -- Add ERI v1 Data Source UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T590005498"] = "Add ERI v1 Data Source" +-- Cannot export this ERI data source because no enterprise encryption secret is configured. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T750361472"] = "Cannot export this ERI data source because no enterprise encryption secret is configured." + -- External Data (ERI-Server v1) UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T774473996"] = "External Data (ERI-Server v1)" +-- Cannot export this ERI data source because no authentication secret is configured. The issue was: {0} +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T782820095"] = "Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}" + -- Local Directory UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T926703547"] = "Local Directory" +-- Export configuration +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T975426229"] = "Export configuration" + -- When enabled, you can preselect some ERI server options. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1280666275"] = "When enabled, you can preselect some ERI server options." @@ -6169,6 +6220,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available" -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." +-- Username provided by the OS +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Username provided by the OS" + -- this version does not met the requirements UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements" @@ -6190,6 +6244,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions" -- Database UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database" +-- This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4060906280"] = "This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication." + -- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system." @@ -6928,6 +6985,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2858189239"] = "Faile -- Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout. UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T286437836"] = "Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout." +-- Failed to read the user's username from the operating system. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2909734556"] = "Failed to read the user's username from the operating system." + -- Failed to retrieve the security requirements due to an exception: {0} UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T3221004295"] = "Failed to retrieve the security requirements due to an exception: {0}" @@ -7504,6 +7564,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T18544701 -- Pandoc may be required for importing files. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Pandoc may be required for importing files." +-- Failed to store the secret data due to an API issue. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Failed to store the secret data due to an API issue." + -- Failed to delete the secret data due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Failed to delete the secret data due to an API issue." diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialog.razor new file mode 100644 index 00000000..088be8ea --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialog.razor @@ -0,0 +1,26 @@ +@inherits MSGComponentBase + +<MudDialog> + <DialogContent> + <MudText Typo="Typo.body1" Class="mb-3"> + @string.Format(T("How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?"), this.DataSource.Name) + </MudText> + + <MudSelect @bind-Value="@this.usernamePasswordMode" Text="@this.GetUsernamePasswordModeText()" Label="@T("Username and password mode")" Class="mt-3 mb-3" OpenIcon="@Icons.Material.Filled.ExpandMore" AdornmentColor="Color.Info" Adornment="Adornment.Start"> + @foreach (var mode in this.availableUsernamePasswordModes) + { + <MudSelectItem Value="@mode"> + @this.GetUsernamePasswordModeText(mode) + </MudSelectItem> + } + </MudSelect> + </DialogContent> + <DialogActions> + <MudButton OnClick="@this.Cancel" Variant="Variant.Filled"> + @T("Cancel") + </MudButton> + <MudButton OnClick="@this.Export" Variant="Variant.Filled" Color="Color.Primary"> + @T("Export") + </MudButton> + </DialogActions> +</MudDialog> \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialog.razor.cs new file mode 100644 index 00000000..cf0ec960 --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialog.razor.cs @@ -0,0 +1,37 @@ +using AIStudio.Components; +using AIStudio.Settings.DataModel; + +using Microsoft.AspNetCore.Components; + +namespace AIStudio.Dialogs; + +public partial class DataSourceERIV1UsernamePasswordExportDialog : MSGComponentBase +{ + [CascadingParameter] + private IMudDialogInstance MudDialog { get; set; } = null!; + + [Parameter] + public DataSourceERI_V1 DataSource { get; set; } + + private readonly DataSourceERIUsernamePasswordMode[] availableUsernamePasswordModes = + [ + DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD, + DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD + ]; + + private DataSourceERIUsernamePasswordMode usernamePasswordMode = DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD; + + private string GetUsernamePasswordModeText() => this.GetUsernamePasswordModeText(this.usernamePasswordMode); + + private string GetUsernamePasswordModeText(DataSourceERIUsernamePasswordMode mode) => mode switch + { + DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD => T("Read each user's username from the operating system and share one password"), + DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD => T("Use the same username and password for all users"), + + _ => T("User-managed username and password"), + }; + + private void Cancel() => this.MudDialog.Cancel(); + + private void Export() => this.MudDialog.Close(DialogResult.Ok(new DataSourceERIV1UsernamePasswordExportDialogResult(this.usernamePasswordMode))); +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialogResult.cs b/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialogResult.cs new file mode 100644 index 00000000..907f920e --- /dev/null +++ b/app/MindWork AI Studio/Dialogs/DataSourceERIV1UsernamePasswordExportDialogResult.cs @@ -0,0 +1,5 @@ +using AIStudio.Settings.DataModel; + +namespace AIStudio.Dialogs; + +public readonly record struct DataSourceERIV1UsernamePasswordExportDialogResult(DataSourceERIUsernamePasswordMode UsernamePasswordMode); \ No newline at end of file diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs index 4a16bd18..8bec772a 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1Dialog.razor.cs @@ -116,7 +116,7 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId if (this.dataAuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD) { // Load the secret: - var requestedSecret = await this.RustService.GetSecret(this); + var requestedSecret = await this.RustService.GetSecret(this, SecretStoreType.DATA_SOURCE); if (requestedSecret.Success) this.dataSecret = await requestedSecret.Secret.Decrypt(this.encryption); else @@ -169,6 +169,7 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId Hostname = cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname, AuthMethod = this.dataAuthMethod, Username = this.dataUsername, + UsernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED, Type = DataSourceType.ERI_V1, SecurityPolicy = this.dataSecurityPolicy, SelectedRetrievalId = this.dataSelectedRetrievalProcess.Id, @@ -323,7 +324,7 @@ public partial class DataSourceERI_V1Dialog : MSGComponentBase, ISecretId if (!string.IsNullOrWhiteSpace(this.dataSecret)) { // Store the secret in the OS secure storage: - var storeResponse = await this.RustService.SetSecret(this, this.dataSecret); + var storeResponse = await this.RustService.SetSecret(this, this.dataSecret, SecretStoreType.DATA_SOURCE); if (!storeResponse.Success) { this.dataSecretStorageIssue = string.Format(T("Failed to store the auth. secret in the operating system. The message was: {0}. Please try again."), storeResponse.Issue); diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1InfoDialog.razor b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1InfoDialog.razor index aa6b7b7d..fa9766fe 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1InfoDialog.razor +++ b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1InfoDialog.razor @@ -21,7 +21,7 @@ @if (this.DataSource.AuthMethod is AuthMethod.USERNAME_PASSWORD) { - <TextInfoLine Icon="@Icons.Material.Filled.Person2" Label="@T("Username")" Value="@this.DataSource.Username" ClipboardTooltipSubject="@T("the username")"/> + <TextInfoLine Icon="@Icons.Material.Filled.Person2" Label="@T("Username")" Value="@this.effectiveUsername" ClipboardTooltipSubject="@T("the username")"/> } <TextInfoLines Label="@T("Server description")" MaxLines="14" Value="@this.serverDescription" ClipboardTooltipSubject="@T("the server description")"/> diff --git a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1InfoDialog.razor.cs b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1InfoDialog.razor.cs index 38ed220a..02d522b6 100644 --- a/app/MindWork AI Studio/Dialogs/DataSourceERI_V1InfoDialog.razor.cs +++ b/app/MindWork AI Studio/Dialogs/DataSourceERI_V1InfoDialog.razor.cs @@ -41,6 +41,7 @@ public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, IAsyncDispos private readonly List<string> dataIssues = []; private string serverDescription = string.Empty; + private string effectiveUsername = string.Empty; private ProviderType securityRequirements = ProviderType.NONE; private IReadOnlyList<RetrievalInfo> retrievalInfoformation = []; private RetrievalInfo selectedRetrievalInfo; @@ -51,6 +52,27 @@ public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, IAsyncDispos private string Port => this.DataSource.Port == 0 ? string.Empty : $"{this.DataSource.Port}"; + private async Task<(bool Success, DataSourceERI_V1 EffectiveDataSource)> CreateEffectiveDataSource() + { + this.effectiveUsername = this.DataSource.Username; + if (this.DataSource is not { AuthMethod: AuthMethod.USERNAME_PASSWORD, UsernamePasswordMode: DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD }) + return (true, this.DataSource); + + var osUsername = await this.RustService.ReadUserName(); + if (string.IsNullOrWhiteSpace(osUsername)) + { + this.dataIssues.Add(T("Failed to read the user's username from the operating system.")); + return (false, this.DataSource); + } + + this.effectiveUsername = osUsername; + return (true, this.DataSource with + { + Username = osUsername, + UsernamePasswordMode = DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD, + }); + } + private string RetrievalName(RetrievalInfo retrievalInfo) { var hasId = !string.IsNullOrWhiteSpace(retrievalInfo.Id); @@ -91,15 +113,19 @@ public partial class DataSourceERI_V1InfoDialog : MSGComponentBase, IAsyncDispos { this.IsOperationInProgress = true; this.StateHasChanged(); + + var effectiveDataSourceResult = await this.CreateEffectiveDataSource(); + if (!effectiveDataSourceResult.Success) + return; - using var client = ERIClientFactory.Get(ERIVersion.V1, this.DataSource); + using var client = ERIClientFactory.Get(ERIVersion.V1, effectiveDataSourceResult.EffectiveDataSource); if(client is null) { this.dataIssues.Add(T("Failed to connect to the ERI v1 server. The server is not supported.")); return; } - var loginResult = await client.AuthenticateAsync(this.RustService); + var loginResult = await client.AuthenticateAsync(this.RustService, cancellationToken: this.cts.Token); if (!loginResult.Successful) { this.dataIssues.Add(loginResult.Message); diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor index 74b15fdb..7755044d 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor @@ -38,12 +38,27 @@ <MudTd> <MudStack Row="true" Class="mb-2 mt-2" Wrap="Wrap.Wrap"> <MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Info" OnClick="() => this.ShowInformation(context)"/> - <MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditDataSource(context)"> - @T("Edit") - </MudButton> - <MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteDataSource(context)"> - @T("Delete") - </MudButton> + @if (context.IsEnterpriseConfiguration) + { + <MudTooltip Text="@T("This data source is managed by your organization.")"> + <MudIconButton Color="Color.Info" Icon="@Icons.Material.Filled.Business" Disabled="true"/> + </MudTooltip> + } + else + { + <MudButton Variant="Variant.Filled" Color="Color.Info" StartIcon="@Icons.Material.Filled.Edit" OnClick="() => this.EditDataSource(context)"> + @T("Edit") + </MudButton> + @if (this.SettingsManager.ConfigurationData.App.ShowAdminSettings && context is DataSourceERI_V1) + { + <MudTooltip Text="@T("Export configuration")"> + <MudIconButton Variant="Variant.Filled" Color="Color.Info" Icon="@Icons.Material.Filled.Dataset" OnClick="() => this.ExportDataSource(context)"/> + </MudTooltip> + } + <MudButton Variant="Variant.Filled" Color="Color.Error" StartIcon="@Icons.Material.Filled.Delete" OnClick="() => this.DeleteDataSource(context)"> + @T("Delete") + </MudButton> + } </MudStack> </MudTd> </RowTemplate> diff --git a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor.cs b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor.cs index c22bed94..ff706363 100644 --- a/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor.cs +++ b/app/MindWork AI Studio/Dialogs/Settings/SettingsDialogDataSources.razor.cs @@ -1,11 +1,17 @@ using AIStudio.Settings; using AIStudio.Settings.DataModel; using AIStudio.Tools.ERIClient.DataModel; +using AIStudio.Tools.PluginSystem; + +using Microsoft.AspNetCore.Components; namespace AIStudio.Dialogs.Settings; public partial class SettingsDialogDataSources : SettingsDialogBase { + [Inject] + private ISnackbar Snackbar { get; init; } = null!; + private string GetEmbeddingName(IDataSource dataSource) { if(dataSource is IInternalDataSource internalDataSource) @@ -86,9 +92,106 @@ public partial class SettingsDialogDataSources : SettingsDialogBase await this.SettingsManager.StoreSettings(); await this.MessageBus.SendMessage<bool>(this, Event.CONFIGURATION_CHANGED); } + + private async Task ExportDataSource(IDataSource dataSource) + { + if (!this.SettingsManager.ConfigurationData.App.ShowAdminSettings) + return; + + if (dataSource is not DataSourceERI_V1 eriDataSource) + return; + + if (eriDataSource.AuthMethod is AuthMethod.KERBEROS) + { + await this.DialogService.ShowMessageBox( + T("Export ERI Data Source"), + T("Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin."), + T("Close")); + return; + } + + var needsSecret = eriDataSource.AuthMethod is AuthMethod.TOKEN or AuthMethod.USERNAME_PASSWORD; + if (!needsSecret) + { + var publicLuaCode = eriDataSource.ExportAsConfigurationSection(); + if (!string.IsNullOrWhiteSpace(publicLuaCode)) + await this.RustService.CopyText2Clipboard(this.Snackbar, publicLuaCode); + + return; + } + + var secretResponse = await this.RustService.GetSecret(eriDataSource, SecretStoreType.DATA_SOURCE, isTrying: true); + if (!secretResponse.Success) + { + await this.DialogService.ShowMessageBox( + T("Export ERI Data Source"), + string.Format(T("Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}"), secretResponse.Issue), + T("Close")); + return; + } + + var encryption = PluginFactory.EnterpriseEncryption; + if (encryption?.IsAvailable != true) + { + await this.DialogService.ShowMessageBox( + T("Export ERI Data Source"), + T("Cannot export this ERI data source because no enterprise encryption secret is configured."), + T("Close")); + return; + } + + var usernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED; + if (eriDataSource.AuthMethod is AuthMethod.TOKEN) + { + var dialogParameters = new DialogParameters<ConfirmDialog> + { + { x => x.Message, T("This ERI data source has an access token configured. Do you want to include the encrypted access token in the export? Note: The recipient will need the same encryption secret to use the access token.") }, + }; + + var dialogReference = await this.DialogService.ShowAsync<ConfirmDialog>(T("Export Access Token?"), dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled) + return; + } + else if (eriDataSource.AuthMethod is AuthMethod.USERNAME_PASSWORD) + { + var dialogParameters = new DialogParameters<DataSourceERIV1UsernamePasswordExportDialog> + { + { x => x.DataSource, eriDataSource }, + }; + + var dialogReference = await this.DialogService.ShowAsync<DataSourceERIV1UsernamePasswordExportDialog>(T("Export ERI Data Source"), dialogParameters, DialogOptions.FULLSCREEN); + var dialogResult = await dialogReference.Result; + if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is not DataSourceERIV1UsernamePasswordExportDialogResult exportResult) + return; + + usernamePasswordMode = exportResult.UsernamePasswordMode; + } + + var decryptedSecret = await secretResponse.Secret.Decrypt(Program.ENCRYPTION); + if (!encryption.TryEncrypt(decryptedSecret, out var encryptedSecret)) + { + await this.DialogService.ShowMessageBox( + T("Export ERI Data Source"), + T("Cannot export this ERI data source because the authentication secret could not be encrypted."), + T("Close")); + return; + } + + var luaCode = eriDataSource.ExportAsConfigurationSection( + encryptedSecret, + usernamePasswordMode); + if (string.IsNullOrWhiteSpace(luaCode)) + return; + + await this.RustService.CopyText2Clipboard(this.Snackbar, luaCode); + } private async Task EditDataSource(IDataSource dataSource) { + if (dataSource.IsEnterpriseConfiguration) + return; + IDataSource? editedDataSource = null; switch (dataSource) { @@ -151,6 +254,9 @@ public partial class SettingsDialogDataSources : SettingsDialogBase private async Task DeleteDataSource(IDataSource dataSource) { + if (dataSource.IsEnterpriseConfiguration) + return; + var dialogParameters = new DialogParameters<ConfirmDialog> { { x => x.Message, string.Format(T("Are you sure you want to delete the data source '{0}' of type {1}?"), dataSource.Name, dataSource.Type.GetDisplayName()) }, @@ -174,7 +280,7 @@ public partial class SettingsDialogDataSources : SettingsDialogBase // All other auth methods require a secret, which we need to delete now: else { - var deleteSecretResponse = await this.RustService.DeleteSecret(externalDataSource); + var deleteSecretResponse = await this.RustService.DeleteSecret(externalDataSource, SecretStoreType.DATA_SOURCE); if (deleteSecretResponse.Success) applyChanges = true; } diff --git a/app/MindWork AI Studio/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Layout/MainLayout.razor.cs index a1659f34..a7a6a8df 100644 --- a/app/MindWork AI Studio/Layout/MainLayout.razor.cs +++ b/app/MindWork AI Studio/Layout/MainLayout.razor.cs @@ -83,7 +83,9 @@ public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver, ILan // Read the user language from Rust: // var userLanguage = await this.RustService.ReadUserLanguage(); + var userName = await this.RustService.ReadUserName(); this.Logger.LogInformation($"The OS says '{userLanguage}' is the user language."); + this.Logger.LogInformation($"The OS says '{userName}' is the username."); // Ensure that all settings are loaded: await this.SettingsManager.LoadSettings(); diff --git a/app/MindWork AI Studio/Pages/Information.razor b/app/MindWork AI Studio/Pages/Information.razor index 119611cb..ae24887d 100644 --- a/app/MindWork AI Studio/Pages/Information.razor +++ b/app/MindWork AI Studio/Pages/Information.razor @@ -47,6 +47,7 @@ <MudListItem T="string" Icon="@Icons.Material.Outlined.Widgets" Text="@MudBlazorVersion"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.Memory" Text="@TauriVersion"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.Translate" Text="@this.OSLanguage"/> + <MudListItem T="string" Icon="@Icons.Material.Outlined.AccountCircle" Text="@this.OSUserName"/> <MudListItem T="string" Icon="@Icons.Material.Outlined.Business"> @switch (HasAnyActiveEnvironment) { @@ -301,6 +302,7 @@ <ThirdPartyComponent Name="PDFium" Developer="Lei Zhang, Tom Sepez, Dan Sinclair, and Foxit, Google, Chromium, Collabora, Ada, DocsCorp, Dropbox, Microsoft, and PSPDFKit Teams & Open Source Community" LicenseName="Apache-2.0" LicenseUrl="https://pdfium.googlesource.com/pdfium/+/refs/heads/main/LICENSE" RepositoryUrl="https://pdfium.googlesource.com/pdfium" UseCase="@T("This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.")"/> <ThirdPartyComponent Name="pdfium-render" Developer="Alastair Carey, Dorian Rudolph & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/ajrcarey/pdfium-render/blob/master/LICENSE.md" RepositoryUrl="https://github.com/ajrcarey/pdfium-render" UseCase="@T("This library is used to read PDF files. This is necessary, e.g., for using PDFs as a data source for a chat.")"/> <ThirdPartyComponent Name="sys-locale" Developer="1Password Team, ComplexSpaces & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/1Password/sys-locale/blob/main/LICENSE-MIT" RepositoryUrl="https://github.com/1Password/sys-locale" UseCase="@T("This library is used to determine the language of the operating system. This is necessary to set the language of the user interface.")"/> + <ThirdPartyComponent Name="whoami" Developer="Ardaku Systems, Jeryn Aldaron Lau, Chase Johnson & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/ardaku/whoami/blob/stable/LICENSE_MIT" RepositoryUrl="https://github.com/ardaku/whoami" UseCase="@T("This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication.")"/> <ThirdPartyComponent Name="sysinfo" Developer="Guillaume Gomez & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/GuillaumeGomez/sysinfo/blob/main/LICENSE" RepositoryUrl="https://github.com/GuillaumeGomez/sysinfo" UseCase="@T("This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated.")"/> <ThirdPartyComponent Name="tempfile" Developer="Steven Allen, Ashley Mannix & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/Stebalien/tempfile/blob/master/LICENSE-MIT" RepositoryUrl="https://github.com/Stebalien/tempfile" UseCase="@T("This library is used to create temporary folders for saving the certificate and private key for communication with Qdrant.")"/> <ThirdPartyComponent Name="Lua-CSharp" Developer="Yusuke Nakada & Open Source Community" LicenseName="MIT" LicenseUrl="https://github.com/nuskey8/Lua-CSharp/blob/main/LICENSE" RepositoryUrl="https://github.com/nuskey8/Lua-CSharp" UseCase="@T("We use Lua as the language for plugins. Lua-CSharp lets Lua scripts communicate with AI Studio and vice versa. Thank you, Yusuke Nakada, for this great library.")" /> diff --git a/app/MindWork AI Studio/Pages/Information.razor.cs b/app/MindWork AI Studio/Pages/Information.razor.cs index 8f2192a5..10a6b614 100644 --- a/app/MindWork AI Studio/Pages/Information.razor.cs +++ b/app/MindWork AI Studio/Pages/Information.razor.cs @@ -40,6 +40,7 @@ public partial class Information : MSGComponentBase private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(Information).Namespace, nameof(Information)); private string osLanguage = string.Empty; + private string osUserName = string.Empty; private static string VersionApp => $"MindWork AI Studio: v{META_DATA.Version} (commit {META_DATA.AppCommitHash}, build {META_DATA.BuildNum}, {META_DATA_ARCH.Architecture.ToRID().ToUserFriendlyName()})"; @@ -49,6 +50,8 @@ public partial class Information : MSGComponentBase private string OSLanguage => $"{T("User-language provided by the OS")}: '{this.osLanguage}'"; + private string OSUserName => $"{T("Username provided by the OS")}: '{this.osUserName}'"; + private string VersionRust => $"{T("Used Rust compiler")}: v{META_DATA.RustVersion}"; private string VersionDotnetRuntime => $"{T("Used .NET runtime")}: v{META_DATA.DotnetVersion}"; @@ -128,6 +131,7 @@ public partial class Information : MSGComponentBase this.RefreshEnterpriseConfigurationState(); this.osLanguage = await this.RustService.ReadUserLanguage(); + this.osUserName = await this.RustService.ReadUserName(); this.logPaths = await this.RustService.GetLogPaths(); await foreach (var (label, value) in this.DatabaseClient.GetDisplayInfo()) diff --git a/app/MindWork AI Studio/Plugins/configuration/plugin.lua b/app/MindWork AI Studio/Plugins/configuration/plugin.lua index b4a942a7..6d2d51d3 100644 --- a/app/MindWork AI Studio/Plugins/configuration/plugin.lua +++ b/app/MindWork AI Studio/Plugins/configuration/plugin.lua @@ -136,6 +136,54 @@ CONFIG["EMBEDDING_PROVIDERS"] = {} -- } -- } +-- ERI v1 data sources for retrieval-augmented generation: +CONFIG["DATA_SOURCES"] = {} + +-- Example: ERI v1 data source with a shared access token. +-- CONFIG["DATA_SOURCES"][#CONFIG["DATA_SOURCES"]+1] = { +-- ["Id"] = "00000000-0000-0000-0000-000000000000", +-- ["Name"] = "<user-friendly data source name>", +-- ["Type"] = "ERI_V1", +-- ["Hostname"] = "<https address of the ERI server>", +-- ["Port"] = 443, +-- ["AuthMethod"] = "TOKEN", +-- ["Token"] = "ENC:v1:<base64-encoded encrypted token>", +-- ["SecurityPolicy"] = "SELF_HOSTED", +-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>", +-- ["MaxMatches"] = 10, +-- } + +-- Example: ERI v1 data source with a shared username and password. +-- CONFIG["DATA_SOURCES"][#CONFIG["DATA_SOURCES"]+1] = { +-- ["Id"] = "00000000-0000-0000-0000-000000000000", +-- ["Name"] = "<user-friendly data source name>", +-- ["Type"] = "ERI_V1", +-- ["Hostname"] = "<https address of the ERI server>", +-- ["Port"] = 443, +-- ["AuthMethod"] = "USERNAME_PASSWORD", +-- ["UsernamePasswordMode"] = "SHARED_USERNAME_AND_PASSWORD", +-- ["Username"] = "<shared username>", +-- ["Password"] = "ENC:v1:<base64-encoded encrypted password>", +-- ["SecurityPolicy"] = "SELF_HOSTED", +-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>", +-- ["MaxMatches"] = 10, +-- } + +-- Example: ERI v1 data source using the user's username and a shared password. +-- CONFIG["DATA_SOURCES"][#CONFIG["DATA_SOURCES"]+1] = { +-- ["Id"] = "00000000-0000-0000-0000-000000000000", +-- ["Name"] = "<user-friendly data source name>", +-- ["Type"] = "ERI_V1", +-- ["Hostname"] = "<https address of the ERI server>", +-- ["Port"] = 443, +-- ["AuthMethod"] = "USERNAME_PASSWORD", +-- ["UsernamePasswordMode"] = "OS_USERNAME_SHARED_PASSWORD", +-- ["Password"] = "ENC:v1:<base64-encoded encrypted password>", +-- ["SecurityPolicy"] = "SELF_HOSTED", +-- ["SelectedRetrievalId"] = "<retrieval process ID from the ERI server>", +-- ["MaxMatches"] = 10, +-- } + CONFIG["SETTINGS"] = {} -- Configure the update check interval: diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index 300ad595..adcba747 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -3633,6 +3633,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2879113658"] = -- Maximum matches per query UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2889706179"] = "Maximale Treffer pro Abfrage" +-- Failed to read the user's username from the operating system. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2909734556"] = "Der Benutzername des Nutzers konnte nicht aus dem Betriebssystem gelesen werden." + -- Open web link, show more information UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2968752071"] = "Weblink öffnen & mehr Informationen anzeigen" @@ -3684,6 +3687,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T742006305"] = " -- Embeddings UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T951463987"] = "Einbettungen" +-- Use the same username and password for all users +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1769874785"] = "Für alle Benutzer denselben Benutzernamen und dasselbe Passwort verwenden" + +-- Username and password mode +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1787063064"] = "Modus für den Benutzernamen und das Passwort" + +-- How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3081234668"] = "Wie soll AI Studio die Konfiguration von Benutzername und Passwort für die ERI-v1-Datenquelle „{0}“ exportieren?" + +-- User-managed username and password +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T365340972"] = "Vom Benutzer verwaltete Anmeldedaten (Benutzername und Passwort)" + +-- Export +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3898821075"] = "Exportieren" + +-- Read each user's username from the operating system and share one password +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T76405695"] = "Den Benutzernamen jedes Benutzers aus dem Betriebssystem auslesen und ein Passwort teilen." + +-- Cancel +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T900713019"] = "Abbrechen" + -- Describe what data this directory contains to help the AI select it. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T1136409150"] = "Beschreiben Sie, welche Daten dieses Verzeichnis enthält, um der KI bei der Auswahl zu helfen." @@ -4812,6 +4836,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T145419 -- Delete UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1469573738"] = "Löschen" +-- Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1577531115"] = "Kerberos-/SSO-ERI-Datenquellen können noch nicht exportiert werden. Bitte konfigurieren Sie diese manuell im Konfigurations-Plugin." + +-- Cannot export this ERI data source because the authentication secret could not be encrypted. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1592527757"] = "Diese ERI-Datenquelle kann nicht exportiert werden, da das Authentifizierungsgeheimnis nicht verschlüsselt werden konnte." + -- External (ERI) UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1652430727"] = "Extern (ERI)" @@ -4842,6 +4872,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T269820 -- Embedding UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T2838542994"] = "Einbettung" +-- This data source is managed by your organization. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3031462878"] = "Diese Datenquelle wird von Ihrer Organisation verwaltet." + -- Edit UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3267849393"] = "Bearbeiten" @@ -4866,21 +4899,39 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T352566 -- No data sources configured yet. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3549650120"] = "Noch keine Datenquellen konfiguriert." +-- Export Access Token? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3595669127"] = "Zugriffstoken exportieren?" + +-- Export ERI Data Source +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3831281036"] = "ERI-Datenquelle exportieren" + -- Actions UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3865031940"] = "Aktionen" +-- This ERI data source has an access token configured. Do you want to include the encrypted access token in the export? Note: The recipient will need the same encryption secret to use the access token. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T4027572258"] = "Für diese ERI-Datenquelle ist ein Zugriffstoken konfiguriert. Möchten Sie das verschlüsselte Zugriffstoken in den Export aufnehmen? Hinweis: Der Empfänger benötigt dasselbe Geheimnis für die Verschlüsselung, um das Zugriffstoken verwenden zu können." + -- Configured Data Sources UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T543942217"] = "Konfigurierte Datenquellen" -- Add ERI v1 Data Source UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T590005498"] = "ERI v1 Datenquelle hinzufügen" +-- Cannot export this ERI data source because no enterprise encryption secret is configured. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T750361472"] = "Diese ERI-Datenquelle kann nicht exportiert werden, da kein Geheimnis für die Verschlüsselung konfiguriert ist." + -- External Data (ERI-Server v1) UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T774473996"] = "Externe Daten (ERI-Server v1)" +-- Cannot export this ERI data source because no authentication secret is configured. The issue was: {0} +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T782820095"] = "Diese ERI-Datenquelle kann nicht exportiert werden, da kein Authentifizierungsgeheimnis konfiguriert ist. Das Problem war: {0}" + -- Local Directory UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T926703547"] = "Lokaler Ordner" +-- Export configuration +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T975426229"] = "Konfiguration exportieren" + -- When enabled, you can preselect some ERI server options. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1280666275"] = "Wenn aktiviert, können Sie einige ERI-Serveroptionen vorauswählen." @@ -6171,6 +6222,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "nicht verfügbar -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "Diese Bibliothek wird verwendet, um Excel- und OpenDocument-Tabellendateien zu lesen. Dies ist zum Beispiel notwendig, wenn Tabellen als Datenquelle für einen Chat verwendet werden sollen." +-- Username provided by the OS +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Vom Betriebssystem bereitgestellter Benutzername" + -- this version does not met the requirements UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "diese Version erfüllt die Anforderungen nicht" @@ -6192,6 +6246,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versionen" -- Database UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Datenbank" +-- This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4060906280"] = "Diese Bibliothek wird von der Rust-Laufzeitumgebung verwendet, um den Benutzernamen des aktuellen Benutzers auszulesen, z. B. wenn ein von einer Organisation verwalteter ERI-Server den OS-Benutzernamen für die Authentifizierung verwendet." + -- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "Diese Bibliothek wird verwendet, um asynchrone Datenströme in Rust zu erstellen. Sie ermöglicht es uns, mit Datenströmen zu arbeiten, die asynchron bereitgestellt werden, wodurch sich Ereignisse oder Daten, die nach und nach eintreffen, leichter verarbeiten lassen. Wir nutzen dies zum Beispiel, um beliebige Daten aus dem Dateisystem an das Einbettungssystem zu übertragen." @@ -6930,6 +6987,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2858189239"] = "Authe -- Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout. UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T286437836"] = "Die Sicherheitsanforderungen konnten nicht abgerufen werden: Die Anfrage wurde entweder vom Benutzer abgebrochen oder ist aufgrund eines Zeitüberschreitungsfehlers fehlgeschlagen." +-- Failed to read the user's username from the operating system. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2909734556"] = "Der Benutzername konnte nicht aus dem Betriebssystem ausgelesen werden." + -- Failed to retrieve the security requirements due to an exception: {0} UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T3221004295"] = "Die Sicherheitsanforderungen konnten wegen eines Problems nicht abgerufen werden: {0}" @@ -7506,6 +7566,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T18544701 -- Pandoc may be required for importing files. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Zum Importieren von Dateien kann Pandoc erforderlich sein." +-- Failed to store the secret data due to an API issue. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Fehler beim Speichern der geheimen Daten aufgrund eines API-Problems." + -- Failed to delete the secret data due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Das Löschen der geheimen Daten ist aufgrund eines API-Problems fehlgeschlagen." diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index e629d833..0f5389cf 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -3633,6 +3633,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2879113658"] = -- Maximum matches per query UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2889706179"] = "Maximum matches per query" +-- Failed to read the user's username from the operating system. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2909734556"] = "Failed to read the user's username from the operating system." + -- Open web link, show more information UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T2968752071"] = "Open web link, show more information" @@ -3684,6 +3687,27 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T742006305"] = " -- Embeddings UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERI_V1INFODIALOG::T951463987"] = "Embeddings" +-- Use the same username and password for all users +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1769874785"] = "Use the same username and password for all users" + +-- Username and password mode +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T1787063064"] = "Username and password mode" + +-- How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3081234668"] = "How should AI Studio export the username and password configuration for the ERI v1 data source '{0}'?" + +-- User-managed username and password +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T365340972"] = "User-managed username and password" + +-- Export +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T3898821075"] = "Export" + +-- Read each user's username from the operating system and share one password +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T76405695"] = "Read each user's username from the operating system and share one password" + +-- Cancel +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCEERIV1USERNAMEPASSWORDEXPORTDIALOG::T900713019"] = "Cancel" + -- Describe what data this directory contains to help the AI select it. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::DATASOURCELOCALDIRECTORYDIALOG::T1136409150"] = "Describe what data this directory contains to help the AI select it." @@ -4812,6 +4836,12 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T145419 -- Delete UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1469573738"] = "Delete" +-- Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1577531115"] = "Kerberos/SSO ERI data sources cannot be exported yet. Please configure them manually in the configuration plugin." + +-- Cannot export this ERI data source because the authentication secret could not be encrypted. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1592527757"] = "Cannot export this ERI data source because the authentication secret could not be encrypted." + -- External (ERI) UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T1652430727"] = "External (ERI)" @@ -4842,6 +4872,9 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T269820 -- Embedding UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T2838542994"] = "Embedding" +-- This data source is managed by your organization. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3031462878"] = "This data source is managed by your organization." + -- Edit UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3267849393"] = "Edit" @@ -4866,21 +4899,39 @@ UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T352566 -- No data sources configured yet. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3549650120"] = "No data sources configured yet." +-- Export Access Token? +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3595669127"] = "Export Access Token?" + +-- Export ERI Data Source +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3831281036"] = "Export ERI Data Source" + -- Actions UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T3865031940"] = "Actions" +-- This ERI data source has an access token configured. Do you want to include the encrypted access token in the export? Note: The recipient will need the same encryption secret to use the access token. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T4027572258"] = "This ERI data source has an access token configured. Do you want to include the encrypted access token in the export? Note: The recipient will need the same encryption secret to use the access token." + -- Configured Data Sources UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T543942217"] = "Configured Data Sources" -- Add ERI v1 Data Source UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T590005498"] = "Add ERI v1 Data Source" +-- Cannot export this ERI data source because no enterprise encryption secret is configured. +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T750361472"] = "Cannot export this ERI data source because no enterprise encryption secret is configured." + -- External Data (ERI-Server v1) UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T774473996"] = "External Data (ERI-Server v1)" +-- Cannot export this ERI data source because no authentication secret is configured. The issue was: {0} +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T782820095"] = "Cannot export this ERI data source because no authentication secret is configured. The issue was: {0}" + -- Local Directory UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T926703547"] = "Local Directory" +-- Export configuration +UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGDATASOURCES::T975426229"] = "Export configuration" + -- When enabled, you can preselect some ERI server options. UI_TEXT_CONTENT["AISTUDIO::DIALOGS::SETTINGS::SETTINGSDIALOGERISERVER::T1280666275"] = "When enabled, you can preselect some ERI server options." @@ -6171,6 +6222,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3574465749"] = "not available" -- This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3722989559"] = "This library is used to read Excel and OpenDocument spreadsheet files. This is necessary, e.g., for using spreadsheets as a data source for a chat." +-- Username provided by the OS +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3764549776"] = "Username provided by the OS" + -- this version does not met the requirements UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T3813932670"] = "this version does not met the requirements" @@ -6192,6 +6246,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4010195468"] = "Versions" -- Database UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4036243672"] = "Database" +-- This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication. +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4060906280"] = "This library is used by the Rust runtime to read the current user's username, e.g. when an organization-managed ERI server uses the OS username for authentication." + -- This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T4079152443"] = "This library is used to create asynchronous streams in Rust. It allows us to work with streams of data that can be produced asynchronously, making it easier to handle events or data that arrive over time. We use this, e.g., to stream arbitrary data from the file system to the embedding system." @@ -6930,6 +6987,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2858189239"] = "Faile -- Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout. UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T286437836"] = "Failed to retrieve the security requirements: the request was canceled either by the user or due to a timeout." +-- Failed to read the user's username from the operating system. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T2909734556"] = "Failed to read the user's username from the operating system." + -- Failed to retrieve the security requirements due to an exception: {0} UI_TEXT_CONTENT["AISTUDIO::TOOLS::ERICLIENT::ERICLIENTV1::T3221004295"] = "Failed to retrieve the security requirements due to an exception: {0}" @@ -7506,6 +7566,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T18544701 -- Pandoc may be required for importing files. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::PANDOCAVAILABILITYSERVICE::T2596465560"] = "Pandoc may be required for importing files." +-- Failed to store the secret data due to an API issue. +UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T1110203516"] = "Failed to store the secret data due to an API issue." + -- Failed to delete the secret data due to an API issue. UI_TEXT_CONTENT["AISTUDIO::TOOLS::SERVICES::RUSTSERVICE::T2303057928"] = "Failed to delete the secret data due to an API issue." diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceERIUsernamePasswordMode.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceERIUsernamePasswordMode.cs new file mode 100644 index 00000000..67a86c41 --- /dev/null +++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceERIUsernamePasswordMode.cs @@ -0,0 +1,19 @@ +namespace AIStudio.Settings.DataModel; + +public enum DataSourceERIUsernamePasswordMode +{ + /// <summary> + /// The user manages the username and password locally. + /// </summary> + USER_MANAGED, + + /// <summary> + /// The username and password are shared by all users and provided by configuration. + /// </summary> + SHARED_USERNAME_AND_PASSWORD, + + /// <summary> + /// The username is read from the operating system, and the password is shared by all users. + /// </summary> + OS_USERNAME_SHARED_PASSWORD, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs index cbc3839c..cd254751 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceERI_V1.cs @@ -4,9 +4,12 @@ using AIStudio.Assistants.ERI; using AIStudio.Chat; using AIStudio.Tools.ERIClient; using AIStudio.Tools.ERIClient.DataModel; +using AIStudio.Tools.PluginSystem; using AIStudio.Tools.RAG; using AIStudio.Tools.Services; +using Lua; + using ChatThread = AIStudio.Chat.ChatThread; using ContentType = AIStudio.Tools.ERIClient.DataModel.ContentType; @@ -17,6 +20,8 @@ namespace AIStudio.Settings.DataModel; /// </summary> public readonly record struct DataSourceERI_V1 : IERIDataSource { + private static readonly ILogger<DataSourceERI_V1> LOGGER = Program.LOGGER_FACTORY.CreateLogger<DataSourceERI_V1>(); + public DataSourceERI_V1() { } @@ -45,8 +50,17 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource /// <inheritdoc /> public string Username { get; init; } = string.Empty; + /// <inheritdoc /> + public DataSourceERIUsernamePasswordMode UsernamePasswordMode { get; init; } = DataSourceERIUsernamePasswordMode.USER_MANAGED; + /// <inheritdoc /> public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED; + + /// <inheritdoc /> + public bool IsEnterpriseConfiguration { get; init; } + + /// <inheritdoc /> + public Guid EnterpriseConfigurationPluginId { get; init; } = Guid.Empty; /// <inheritdoc /> public ERIVersion Version { get; init; } = ERIVersion.V1; @@ -82,7 +96,7 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource Thread = await thread.ToERIChatThread(token), MaxMatches = this.MaxMatches, - RetrievalProcessId = string.IsNullOrWhiteSpace(this.SelectedRetrievalId) ? null : this.SelectedRetrievalId, + RetrievalProcessId = this.SelectedRetrievalId, Parameters = null, // The ERI server selects useful default parameters }; @@ -139,4 +153,240 @@ public readonly record struct DataSourceERI_V1 : IERIDataSource logger.LogWarning($"Was not able to authenticate with the ERI data source '{this.Name}'. Message: {authResponse.Message}"); return []; } + + public static bool TryParseConfiguration(int idx, LuaTable table, Guid configPluginId, out DataSourceERI_V1 dataSource) + { + dataSource = default; + if (!table.TryGetValue("Id", out var idValue) || !idValue.TryRead<string>(out var idText) || !Guid.TryParse(idText, out var id)) + { + LOGGER.LogWarning($"The configured data source {idx} does not contain a valid ID. The ID must be a valid GUID. (Plugin ID: {configPluginId})"); + return false; + } + + if (!table.TryGetValue("Name", out var nameValue) || !nameValue.TryRead<string>(out var name) || string.IsNullOrWhiteSpace(name)) + { + LOGGER.LogWarning($"The configured data source {idx} does not contain a valid name. (Plugin ID: {configPluginId})"); + return false; + } + + if (!table.TryGetValue("Type", out var typeValue) || !typeValue.TryRead<string>(out var typeText) || !Enum.TryParse<DataSourceType>(typeText, true, out var type) || type is not DataSourceType.ERI_V1) + { + LOGGER.LogWarning($"The configured data source {idx} does not contain a supported data source type. Only ERI_V1 is supported. (Plugin ID: {configPluginId})"); + return false; + } + + if (!table.TryGetValue("Hostname", out var hostnameValue) || !hostnameValue.TryRead<string>(out var hostname) || string.IsNullOrWhiteSpace(hostname)) + { + LOGGER.LogWarning($"The configured data source {idx} does not contain a valid hostname. (Plugin ID: {configPluginId})"); + return false; + } + + if (!table.TryGetValue("Port", out var portValue) || !portValue.TryRead<int>(out var port) || port is < 1 or > 65535) + { + LOGGER.LogWarning($"The configured data source {idx} does not contain a valid port. (Plugin ID: {configPluginId})"); + return false; + } + + if (!table.TryGetValue("AuthMethod", out var authMethodValue) || !authMethodValue.TryRead<string>(out var authMethodText) || !Enum.TryParse<AuthMethod>(authMethodText, true, out var authMethod)) + { + LOGGER.LogWarning($"The configured data source {idx} does not contain a valid auth method. (Plugin ID: {configPluginId})"); + return false; + } + + if (!table.TryGetValue("SecurityPolicy", out var securityPolicyValue) || !securityPolicyValue.TryRead<string>(out var securityPolicyText) || !Enum.TryParse<DataSourceSecurity>(securityPolicyText, true, out var securityPolicy)) + { + LOGGER.LogWarning($"The configured data source {idx} does not contain a valid security policy. (Plugin ID: {configPluginId})"); + return false; + } + + if (securityPolicy is DataSourceSecurity.NOT_SPECIFIED) + { + LOGGER.LogWarning($"The configured data source {idx} must specify a security policy. (Plugin ID: {configPluginId})"); + return false; + } + + if (!table.TryGetValue("SelectedRetrievalId", out var selectedRetrievalIdValue) || !selectedRetrievalIdValue.TryRead<string>(out var selectedRetrievalId) || string.IsNullOrWhiteSpace(selectedRetrievalId)) + { + LOGGER.LogWarning($"The configured data source {idx} must specify a selected retrieval ID. (Plugin ID: {configPluginId})"); + return false; + } + + if (!table.TryGetValue("MaxMatches", out var maxMatchesValue) || !maxMatchesValue.TryRead<int>(out var maxMatches) || maxMatches is < 1 or > ushort.MaxValue) + { + LOGGER.LogWarning($"The configured data source {idx} does not contain a valid maximum number of matches. (Plugin ID: {configPluginId})"); + return false; + } + + var username = string.Empty; + var usernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED; + if (table.TryGetValue("UsernamePasswordMode", out var usernamePasswordModeValue) && usernamePasswordModeValue.TryRead<string>(out var usernamePasswordModeText)) + { + if (!Enum.TryParse(usernamePasswordModeText, true, out usernamePasswordMode)) + { + LOGGER.LogWarning($"The configured data source {idx} does not contain a valid username/password mode. (Plugin ID: {configPluginId})"); + return false; + } + + if (usernamePasswordMode is DataSourceERIUsernamePasswordMode.USER_MANAGED) + { + LOGGER.LogWarning($"The configured data source {idx} uses the user-managed username/password mode. This mode is not allowed in configuration plugins. (Plugin ID: {configPluginId})"); + return false; + } + } + + if (authMethod is AuthMethod.USERNAME_PASSWORD) + { + if (!table.TryGetValue("UsernamePasswordMode", out _) || usernamePasswordMode is DataSourceERIUsernamePasswordMode.USER_MANAGED) + { + LOGGER.LogWarning($"The configured data source {idx} must specify an organization-managed username/password mode. (Plugin ID: {configPluginId})"); + return false; + } + + if (usernamePasswordMode is DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD && + (!table.TryGetValue("Username", out var usernameValue) || !usernameValue.TryRead<string>(out username) || string.IsNullOrWhiteSpace(username))) + { + LOGGER.LogWarning($"The configured data source {idx} must specify a username. (Plugin ID: {configPluginId})"); + return false; + } + } + + dataSource = new DataSourceERI_V1 + { + Num = 0, + Id = id.ToString(), + Name = name, + Type = DataSourceType.ERI_V1, + Hostname = CleanHostname(hostname), + Port = port, + AuthMethod = authMethod, + Username = username, + UsernamePasswordMode = usernamePasswordMode, + SecurityPolicy = securityPolicy, + Version = ERIVersion.V1, + SelectedRetrievalId = selectedRetrievalId, + MaxMatches = (ushort)maxMatches, + IsEnterpriseConfiguration = true, + EnterpriseConfigurationPluginId = configPluginId, + }; + + return TryQueueEnterpriseSecret(idx, table, configPluginId, dataSource); + } + + /// <summary> + /// Exports the ERI v1 data source configuration as a Lua configuration section. + /// </summary> + /// <param name="encryptedSecret">Optional encrypted token or password to include in the export.</param> + /// <param name="usernamePasswordMode">The organization-managed username/password mode to export.</param> + /// <returns>A Lua configuration section string.</returns> + public string ExportAsConfigurationSection(string? encryptedSecret = null, DataSourceERIUsernamePasswordMode usernamePasswordMode = DataSourceERIUsernamePasswordMode.USER_MANAGED) + { + var secretLine = string.Empty; + var usernamePasswordModeLine = string.Empty; + var usernameLine = string.Empty; + + switch (this.AuthMethod) + { + case AuthMethod.TOKEN: + secretLine = CreateSecretLine("Token", encryptedSecret); + break; + + case AuthMethod.USERNAME_PASSWORD: + if (usernamePasswordMode is DataSourceERIUsernamePasswordMode.USER_MANAGED) + usernamePasswordMode = DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD; + + usernamePasswordModeLine = $""" + ["UsernamePasswordMode"] = "{usernamePasswordMode}", + """; + + if (usernamePasswordMode is DataSourceERIUsernamePasswordMode.SHARED_USERNAME_AND_PASSWORD) + { + var username = string.IsNullOrWhiteSpace(this.Username) ? "<shared username>" : this.Username; + usernameLine = $""" + ["Username"] = "{LuaTools.EscapeLuaString(username)}", + """; + } + + secretLine = CreateSecretLine("Password", encryptedSecret); + break; + } + + return $$""" + CONFIG["DATA_SOURCES"][#CONFIG["DATA_SOURCES"]+1] = { + ["Id"] = "{{Guid.NewGuid().ToString()}}", + ["Name"] = "{{LuaTools.EscapeLuaString(this.Name)}}", + ["Type"] = "ERI_V1", + ["Hostname"] = "{{LuaTools.EscapeLuaString(this.Hostname)}}", + ["Port"] = {{this.Port}}, + ["AuthMethod"] = "{{this.AuthMethod}}", + {{usernamePasswordModeLine}} + {{usernameLine}} + {{secretLine}} + ["SecurityPolicy"] = "{{this.SecurityPolicy}}", + ["SelectedRetrievalId"] = "{{LuaTools.EscapeLuaString(this.SelectedRetrievalId)}}", + ["MaxMatches"] = {{this.MaxMatches}}, + } + """; + } + + private static bool TryQueueEnterpriseSecret(int idx, LuaTable table, Guid configPluginId, DataSourceERI_V1 dataSource) + { + var secretFieldName = dataSource.AuthMethod switch + { + AuthMethod.TOKEN => "Token", + AuthMethod.USERNAME_PASSWORD => "Password", + _ => string.Empty, + }; + + if (string.IsNullOrWhiteSpace(secretFieldName)) + return true; + + if (!table.TryGetValue(secretFieldName, out var secretValue) || !secretValue.TryRead<string>(out var encryptedSecret) || string.IsNullOrWhiteSpace(encryptedSecret)) + { + LOGGER.LogWarning($"The configured data source {idx} does not contain a valid encrypted {secretFieldName}. (Plugin ID: {configPluginId})"); + return false; + } + + if (!EnterpriseEncryption.IsEncrypted(encryptedSecret)) + { + LOGGER.LogWarning($"The configured data source {idx} contains a plaintext {secretFieldName}. Only encrypted secrets (starting with 'ENC:v1:') are supported. (Plugin ID: {configPluginId})"); + return false; + } + + var encryption = PluginFactory.EnterpriseEncryption; + if (encryption?.IsAvailable != true) + { + LOGGER.LogWarning($"The configured data source {idx} contains an encrypted {secretFieldName}, but no encryption secret is configured. (Plugin ID: {configPluginId})"); + return false; + } + + if (!encryption.TryDecrypt(encryptedSecret, out var decryptedSecret)) + { + LOGGER.LogWarning($"Failed to decrypt the {secretFieldName} for data source {idx}. The encryption secret may be incorrect. (Plugin ID: {configPluginId})"); + return false; + } + + PendingEnterpriseSecrets.Add(new( + $"{ISecretId.ENTERPRISE_KEY_PREFIX}::{dataSource.Id}", + dataSource.Name, + decryptedSecret, + SecretStoreType.DATA_SOURCE)); + LOGGER.LogDebug($"Successfully decrypted the {secretFieldName} for data source {idx}. It will be stored in the OS keyring. (Plugin ID: {configPluginId})"); + return true; + } + + private static string CreateSecretLine(string fieldName, string? encryptedSecret) + { + if (string.IsNullOrWhiteSpace(encryptedSecret)) + return string.Empty; + + return $""" + ["{fieldName}"] = "{LuaTools.EscapeLuaString(encryptedSecret)}", + """; + } + + private static string CleanHostname(string hostname) + { + var cleanedHostname = hostname.Trim(); + return cleanedHostname.EndsWith('/') ? cleanedHostname[..^1] : cleanedHostname; + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalDirectory.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalDirectory.cs index d8b263c3..a7531e74 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalDirectory.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalDirectory.cs @@ -35,6 +35,12 @@ public readonly record struct DataSourceLocalDirectory : IInternalDataSource /// <inheritdoc /> public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED; + + /// <inheritdoc /> + public bool IsEnterpriseConfiguration { get; init; } + + /// <inheritdoc /> + public Guid EnterpriseConfigurationPluginId { get; init; } = Guid.Empty; /// <inheritdoc /> public ushort MaxMatches { get; init; } = 10; diff --git a/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalFile.cs b/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalFile.cs index 11b857d0..0df0790f 100644 --- a/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalFile.cs +++ b/app/MindWork AI Studio/Settings/DataModel/DataSourceLocalFile.cs @@ -35,6 +35,12 @@ public readonly record struct DataSourceLocalFile : IInternalDataSource /// <inheritdoc /> public DataSourceSecurity SecurityPolicy { get; init; } = DataSourceSecurity.NOT_SPECIFIED; + + /// <inheritdoc /> + public bool IsEnterpriseConfiguration { get; init; } + + /// <inheritdoc /> + public Guid EnterpriseConfigurationPluginId { get; init; } = Guid.Empty; /// <inheritdoc /> public ushort MaxMatches { get; init; } = 10; diff --git a/app/MindWork AI Studio/Settings/IDataSource.cs b/app/MindWork AI Studio/Settings/IDataSource.cs index 9ce3dc9f..7e29123d 100644 --- a/app/MindWork AI Studio/Settings/IDataSource.cs +++ b/app/MindWork AI Studio/Settings/IDataSource.cs @@ -2,6 +2,7 @@ using System.Text.Json.Serialization; using AIStudio.Chat; using AIStudio.Settings.DataModel; +using AIStudio.Tools.PluginSystem; using AIStudio.Tools.RAG; namespace AIStudio.Settings; @@ -13,23 +14,8 @@ namespace AIStudio.Settings; [JsonDerivedType(typeof(DataSourceLocalDirectory), nameof(DataSourceType.LOCAL_DIRECTORY))] [JsonDerivedType(typeof(DataSourceLocalFile), nameof(DataSourceType.LOCAL_FILE))] [JsonDerivedType(typeof(DataSourceERI_V1), nameof(DataSourceType.ERI_V1))] -public interface IDataSource +public interface IDataSource : IConfigurationObject { - /// <summary> - /// The number of the data source. - /// </summary> - public uint Num { get; init; } - - /// <summary> - /// The unique identifier of the data source. - /// </summary> - public string Id { get; init; } - - /// <summary> - /// The name of the data source. - /// </summary> - public string Name { get; init; } - /// <summary> /// Which type of data source is this? /// </summary> diff --git a/app/MindWork AI Studio/Settings/IERIDataSource.cs b/app/MindWork AI Studio/Settings/IERIDataSource.cs index 55138978..40dd625d 100644 --- a/app/MindWork AI Studio/Settings/IERIDataSource.cs +++ b/app/MindWork AI Studio/Settings/IERIDataSource.cs @@ -1,4 +1,5 @@ using AIStudio.Assistants.ERI; +using AIStudio.Settings.DataModel; using AIStudio.Tools.ERIClient.DataModel; namespace AIStudio.Settings; @@ -24,6 +25,11 @@ public interface IERIDataSource : IExternalDataSource /// The username to use for authentication, when the auth. method is USERNAME_PASSWORD. /// </summary> public string Username { get; init; } + + /// <summary> + /// How username/password authentication should obtain the username. + /// </summary> + public DataSourceERIUsernamePasswordMode UsernamePasswordMode { get; init; } /// <summary> /// The ERI specification to use. diff --git a/app/MindWork AI Studio/Settings/IExternalDataSource.cs b/app/MindWork AI Studio/Settings/IExternalDataSource.cs index 8a7c067c..6b75fa56 100644 --- a/app/MindWork AI Studio/Settings/IExternalDataSource.cs +++ b/app/MindWork AI Studio/Settings/IExternalDataSource.cs @@ -7,7 +7,7 @@ public interface IExternalDataSource : IDataSource, ISecretId #region Implementation of ISecretId [JsonIgnore] - string ISecretId.SecretId => this.Id; + string ISecretId.SecretId => this.IsEnterpriseConfiguration ? $"{ENTERPRISE_KEY_PREFIX}::{this.Id}" : this.Id; [JsonIgnore] string ISecretId.SecretName => this.Name; diff --git a/app/MindWork AI Studio/Tools/ERIClient/ERIClientV1.cs b/app/MindWork AI Studio/Tools/ERIClient/ERIClientV1.cs index 2653ca2a..f00976b9 100644 --- a/app/MindWork AI Studio/Tools/ERIClient/ERIClientV1.cs +++ b/app/MindWork AI Studio/Tools/ERIClient/ERIClientV1.cs @@ -2,6 +2,7 @@ using System.Text; using System.Text.Json; using AIStudio.Settings; +using AIStudio.Settings.DataModel; using AIStudio.Tools.ERIClient.DataModel; using AIStudio.Tools.PluginSystem; using AIStudio.Tools.Services; @@ -102,10 +103,23 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource), } case AuthMethod.USERNAME_PASSWORD: + if (this.DataSource.UsernamePasswordMode is DataSourceERIUsernamePasswordMode.OS_USERNAME_SHARED_PASSWORD) + { + username = await rustService.ReadUserName(); + if (string.IsNullOrWhiteSpace(username)) + { + return new() + { + Successful = false, + Message = TB("Failed to read the user's username from the operating system.") + }; + } + } + string password; if (string.IsNullOrWhiteSpace(temporarySecret)) { - var passwordResponse = await rustService.GetSecret(this.DataSource); + var passwordResponse = await rustService.GetSecret(this.DataSource, SecretStoreType.DATA_SOURCE); if (!passwordResponse.Success) { return new() @@ -159,7 +173,7 @@ public class ERIClientV1(IERIDataSource dataSource) : ERIClientBase(dataSource), string token; if (string.IsNullOrWhiteSpace(temporarySecret)) { - var tokenResponse = await rustService.GetSecret(this.DataSource); + var tokenResponse = await rustService.GetSecret(this.DataSource, SecretStoreType.DATA_SOURCE); if (!tokenResponse.Success) { return new() diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PendingEnterpriseApiKey.cs b/app/MindWork AI Studio/Tools/PluginSystem/PendingEnterpriseApiKey.cs index 5f1cb58b..63b7ebfb 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PendingEnterpriseApiKey.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PendingEnterpriseApiKey.cs @@ -13,37 +13,4 @@ public sealed record PendingEnterpriseApiKey( string SecretId, string SecretName, string ApiKey, - SecretStoreType StoreType); - -/// <summary> -/// Static container for pending API keys during plugin loading. -/// </summary> -public static class PendingEnterpriseApiKeys -{ - private static readonly List<PendingEnterpriseApiKey> PENDING_KEYS = []; - private static readonly Lock LOCK = new(); - - /// <summary> - /// Adds a pending API key to the list. - /// </summary> - /// <param name="key">The pending API key to add.</param> - public static void Add(PendingEnterpriseApiKey key) - { - lock (LOCK) - PENDING_KEYS.Add(key); - } - - /// <summary> - /// Gets and clears all pending API keys. - /// </summary> - /// <returns>A list of all pending API keys.</returns> - public static IReadOnlyList<PendingEnterpriseApiKey> GetAndClear() - { - lock (LOCK) - { - var keys = PENDING_KEYS.ToList(); - PENDING_KEYS.Clear(); - return keys; - } - } -} + SecretStoreType StoreType); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PendingEnterpriseApiKeys.cs b/app/MindWork AI Studio/Tools/PluginSystem/PendingEnterpriseApiKeys.cs new file mode 100644 index 00000000..8824424e --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/PendingEnterpriseApiKeys.cs @@ -0,0 +1,34 @@ +namespace AIStudio.Tools.PluginSystem; + +/// <summary> +/// Static container for pending API keys during plugin loading. +/// </summary> +public static class PendingEnterpriseApiKeys +{ + private static readonly List<PendingEnterpriseApiKey> PENDING_KEYS = []; + private static readonly Lock LOCK = new(); + + /// <summary> + /// Adds a pending API key to the list. + /// </summary> + /// <param name="key">The pending API key to add.</param> + public static void Add(PendingEnterpriseApiKey key) + { + lock (LOCK) + PENDING_KEYS.Add(key); + } + + /// <summary> + /// Gets and clears all pending API keys. + /// </summary> + /// <returns>A list of all pending API keys.</returns> + public static IReadOnlyList<PendingEnterpriseApiKey> GetAndClear() + { + lock (LOCK) + { + var keys = PENDING_KEYS.ToList(); + PENDING_KEYS.Clear(); + return keys; + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PendingEnterpriseSecret.cs b/app/MindWork AI Studio/Tools/PluginSystem/PendingEnterpriseSecret.cs new file mode 100644 index 00000000..d4be88e4 --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/PendingEnterpriseSecret.cs @@ -0,0 +1,14 @@ +namespace AIStudio.Tools.PluginSystem; + +/// <summary> +/// Represents a pending enterprise secret that needs to be stored in the OS keyring. +/// </summary> +/// <param name="SecretId">The secret ID.</param> +/// <param name="SecretName">The secret name.</param> +/// <param name="SecretData">The decrypted secret data.</param> +/// <param name="StoreType">The type of secret store to use.</param> +public sealed record PendingEnterpriseSecret( + string SecretId, + string SecretName, + string SecretData, + SecretStoreType StoreType); \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PendingEnterpriseSecrets.cs b/app/MindWork AI Studio/Tools/PluginSystem/PendingEnterpriseSecrets.cs new file mode 100644 index 00000000..1fef45fa --- /dev/null +++ b/app/MindWork AI Studio/Tools/PluginSystem/PendingEnterpriseSecrets.cs @@ -0,0 +1,34 @@ +namespace AIStudio.Tools.PluginSystem; + +/// <summary> +/// Static container for pending enterprise secrets during plugin loading. +/// </summary> +public static class PendingEnterpriseSecrets +{ + private static readonly List<PendingEnterpriseSecret> PENDING_SECRETS = []; + private static readonly Lock LOCK = new(); + + /// <summary> + /// Adds a pending enterprise secret to the list. + /// </summary> + /// <param name="secret">The pending enterprise secret to add.</param> + public static void Add(PendingEnterpriseSecret secret) + { + lock (LOCK) + PENDING_SECRETS.Add(secret); + } + + /// <summary> + /// Gets and clears all pending enterprise secrets. + /// </summary> + /// <returns>A list of all pending enterprise secrets.</returns> + public static IReadOnlyList<PendingEnterpriseSecret> GetAndClear() + { + lock (LOCK) + { + var secrets = PENDING_SECRETS.ToList(); + PENDING_SECRETS.Clear(); + return secrets; + } + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs index da504b29..77391601 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfiguration.cs @@ -39,12 +39,43 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT { // Store any decrypted API keys from enterprise configuration in the OS keyring: await StoreEnterpriseApiKeysAsync(); + await StoreEnterpriseSecretsAsync(); await SETTINGS_MANAGER.StoreSettings(); await MessageBus.INSTANCE.SendMessage<bool>(null, Event.CONFIGURATION_CHANGED); } } + /// <summary> + /// Stores any pending enterprise secrets in the OS keyring. + /// </summary> + private static async Task StoreEnterpriseSecretsAsync() + { + var pendingSecrets = PendingEnterpriseSecrets.GetAndClear(); + if (pendingSecrets.Count == 0) + return; + + LOG.LogInformation($"Storing {pendingSecrets.Count} enterprise secret(s) in the OS keyring."); + var rustService = Program.SERVICE_PROVIDER.GetRequiredService<RustService>(); + foreach (var pendingSecret in pendingSecrets) + { + try + { + var secretId = new TemporarySecretId(pendingSecret.SecretId, pendingSecret.SecretName); + var result = await rustService.SetSecret(secretId, pendingSecret.SecretData, pendingSecret.StoreType); + + if (result.Success) + LOG.LogDebug($"Successfully stored enterprise secret for '{pendingSecret.SecretName}' in the OS keyring."); + else + LOG.LogWarning($"Failed to store enterprise secret for '{pendingSecret.SecretName}': {result.Issue}"); + } + catch (Exception ex) + { + LOG.LogError(ex, $"Exception while storing enterprise secret for '{pendingSecret.SecretName}'."); + } + } + } + /// <summary> /// Stores any pending enterprise API keys in the OS keyring. /// </summary> @@ -152,6 +183,9 @@ public sealed class PluginConfiguration(bool isInternal, LuaState state, PluginT // Handle configured chat templates: PluginConfigurationObject.TryParse(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, x => x.NextChatTemplateNum, mainTable, this.Id, ref this.configObjects, dryRun); + + // Handle configured data sources: + PluginConfigurationObject.TryParseDataSources(mainTable, this.Id, ref this.configObjects, dryRun); // Handle configured profiles: PluginConfigurationObject.TryParse(PluginConfigurationObjectType.PROFILE, x => x.Profiles, x => x.NextProfileNum, mainTable, this.Id, ref this.configObjects, dryRun); diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs index d0b299d3..26f10e7d 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginConfigurationObject.cs @@ -162,6 +162,87 @@ public sealed record PluginConfigurationObject return true; } + /// <summary> + /// Parses configured data sources from a configuration plugin. + /// </summary> + /// <param name="mainTable">The Lua table containing entries to parse into data sources.</param> + /// <param name="configPluginId">The unique identifier of the plugin associated with the data sources.</param> + /// <param name="configObjects">The list to populate with the parsed configuration objects.</param> + /// <param name="dryRun">Specifies whether to perform the operation as a dry run.</param> + /// <returns>True if the table was present and processed; otherwise false.</returns> + public static bool TryParseDataSources( + LuaTable mainTable, + Guid configPluginId, + ref List<PluginConfigurationObject> configObjects, + bool dryRun) + { + const string LUA_TABLE_NAME = "DATA_SOURCES"; + if (!mainTable.TryGetValue(LUA_TABLE_NAME, out var luaValue) || !luaValue.TryRead<LuaTable>(out var luaTable)) + { + LOG.LogWarning("The table '{LuaTableName}' does not exist or is not a valid table (config plugin id: {ConfigPluginId}).", LUA_TABLE_NAME, configPluginId); + return false; + } + + var storedObjects = SETTINGS_MANAGER.ConfigurationData.DataSources; + var numberObjects = luaTable.ArrayLength; + ThreadSafeRandom? random = null; + for (var i = 1; i <= numberObjects; i++) + { + var luaObjectTableValue = luaTable[i]; + if (!luaObjectTableValue.TryRead<LuaTable>(out var luaObjectTable)) + { + LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} is not a valid table (config plugin id: {ConfigPluginId}).", LUA_TABLE_NAME, i, configPluginId); + continue; + } + + if (!DataSourceERI_V1.TryParseConfiguration(i, luaObjectTable, configPluginId, out var configObject)) + { + LOG.LogWarning("The table '{LuaTableName}' entry at index {Index} does not contain a valid data source (config plugin id: {ConfigPluginId}).", LUA_TABLE_NAME, i, configPluginId); + continue; + } + + configObjects.Add(new() + { + ConfigPluginId = configPluginId, + Id = Guid.Parse(configObject.Id), + Type = PluginConfigurationObjectType.DATA_SOURCE, + }); + + if (dryRun) + continue; + + var objectIndex = storedObjects.FindIndex(t => t.Id == configObject.Id); + if (objectIndex > -1) + { + var existingObject = storedObjects[objectIndex]; + configObject = configObject with { Num = existingObject.Num }; + storedObjects[objectIndex] = configObject; + } + else + { + if (IncrementDataSourceNum() is { Success: true, UpdatedValue: var nextNum }) + { + configObject = configObject with { Num = nextNum }; + storedObjects.Add(configObject); + } + else + { + random ??= new ThreadSafeRandom(); + configObject = configObject with { Num = (uint)random.Next(500_000, 1_000_000) }; + storedObjects.Add(configObject); + LOG.LogWarning("The next number for the data source '{ConfigObjectName}' (id={ConfigObjectId}) could not be incremented. Using a random number instead (config plugin id: {ConfigPluginId}).", configObject.Name, configObject.Id, configPluginId); + } + } + } + + return true; + + static IncrementResult<uint> IncrementDataSourceNum() + { + return ((Expression<Func<Data, uint>>)(x => x.NextDataSourceNum)).TryIncrement(SETTINGS_MANAGER.ConfigurationData, IncrementType.POST); + } + } + /// <summary> /// Cleans up configuration objects of a specified type that are no longer associated with any available plugin. /// </summary> @@ -171,13 +252,15 @@ public sealed record PluginConfigurationObject /// <param name="availablePlugins">A list of currently available plugins.</param> /// <param name="configObjectList">A list of all existing configuration objects.</param> /// <param name="secretStoreType">An optional parameter specifying the type of secret store to use for deleting associated API keys from the OS keyring, if applicable.</param> + /// <param name="deleteSecret">When true, delete the associated non-API-key secret from the OS keyring.</param> /// <returns>Returns true if the configuration was altered during cleanup; otherwise, false.</returns> public static async Task<bool> CleanLeftOverConfigurationObjects<TClass>( PluginConfigurationObjectType configObjectType, Expression<Func<Data, List<TClass>>> configObjectSelection, IList<IAvailablePlugin> availablePlugins, IList<PluginConfigurationObject> configObjectList, - SecretStoreType? secretStoreType = null) where TClass : IConfigurationObject + SecretStoreType? secretStoreType = null, + bool deleteSecret = false) where TClass : IConfigurationObject { var configuredObjects = configObjectSelection.Compile()(SETTINGS_MANAGER.ConfigurationData); var leftOverObjects = new List<TClass>(); @@ -220,7 +303,15 @@ public sealed record PluginConfigurationObject configuredObjects.Remove(item); // Delete the API key from the OS keyring if the removed object has one: - if(secretStoreType is not null && item is ISecretId secretId) + if(deleteSecret && item is ISecretId regularSecretId) + { + var deleteResult = await RUST_SERVICE.DeleteSecret(regularSecretId, secretStoreType ?? SecretStoreType.DATA_SOURCE); + if (deleteResult.Success) + LOG.LogInformation($"Successfully deleted secret for removed enterprise object '{item.Name}' from the OS keyring."); + else + LOG.LogWarning($"Failed to delete secret for removed enterprise object '{item.Name}' from the OS keyring: {deleteResult.Issue}"); + } + else if(secretStoreType is not null && item is ISecretId secretId) { var deleteResult = await RUST_SERVICE.DeleteAPIKey(secretId, secretStoreType.Value); if (deleteResult.Success) diff --git a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs index aedc7f7e..d09eaf34 100644 --- a/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs +++ b/app/MindWork AI Studio/Tools/PluginSystem/PluginFactory.Loading.cs @@ -174,6 +174,10 @@ public static partial class PluginFactory if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.EMBEDDING_PROVIDER, x => x.EmbeddingProviders, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.EMBEDDING_PROVIDER)) wasConfigurationChanged = true; + // Check data sources: + if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.DATA_SOURCE, x => x.DataSources, AVAILABLE_PLUGINS, configObjectList, SecretStoreType.DATA_SOURCE, deleteSecret: true)) + wasConfigurationChanged = true; + // Check chat templates: if(await PluginConfigurationObject.CleanLeftOverConfigurationObjects(PluginConfigurationObjectType.CHAT_TEMPLATE, x => x.ChatTemplates, AVAILABLE_PLUGINS, configObjectList)) wasConfigurationChanged = true; diff --git a/app/MindWork AI Studio/Tools/SecretStoreType.cs b/app/MindWork AI Studio/Tools/SecretStoreType.cs index c4382b7b..5e9182d7 100644 --- a/app/MindWork AI Studio/Tools/SecretStoreType.cs +++ b/app/MindWork AI Studio/Tools/SecretStoreType.cs @@ -1,10 +1,10 @@ namespace AIStudio.Tools; /// <summary> -/// Represents the type of secret store used for API keys. +/// Represents the type of secret store used for API keys and other secrets. /// </summary> /// <remarks> -/// Different provider types use different prefixes for storing API keys. +/// Different provider and secret types use different prefixes for storing secrets. /// This prevents collisions when the same instance name is used across /// different provider types (e.g., LLM, Embedding, Transcription). /// </remarks> @@ -29,4 +29,9 @@ public enum SecretStoreType /// Image provider secrets. Uses the "image::" prefix. /// </summary> IMAGE_PROVIDER, -} + + /// <summary> + /// Data source secrets. Uses the "data-source::" prefix. + /// </summary> + DATA_SOURCE, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/SecretStoreTypeExtensions.cs b/app/MindWork AI Studio/Tools/SecretStoreTypeExtensions.cs index d0d4ba9e..5e8ae2f0 100644 --- a/app/MindWork AI Studio/Tools/SecretStoreTypeExtensions.cs +++ b/app/MindWork AI Studio/Tools/SecretStoreTypeExtensions.cs @@ -9,12 +9,14 @@ public static class SecretStoreTypeExtensions /// LLM_PROVIDER uses the legacy "provider" prefix for backward compatibility. /// </remarks> /// <param name="type">The SecretStoreType enum value.</param> - /// <returns>>The corresponding prefix string.</returns> + /// <returns>The corresponding prefix string.</returns> public static string Prefix(this SecretStoreType type) => type switch { SecretStoreType.LLM_PROVIDER => "provider", SecretStoreType.EMBEDDING_PROVIDER => "embedding", SecretStoreType.TRANSCRIPTION_PROVIDER => "transcription", + SecretStoreType.IMAGE_PROVIDER => "image", + SecretStoreType.DATA_SOURCE => "data-source", _ => "provider", }; diff --git a/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs b/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs index 656d7358..6db55a6c 100644 --- a/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs +++ b/app/MindWork AI Studio/Tools/Services/EnterpriseEnvironmentService.cs @@ -200,7 +200,7 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe { logger.LogInformation("The enterprise encryption secret changed. Refreshing the enterprise encryption service and reloading plugins."); PluginFactory.InitializeEnterpriseEncryption(enterpriseEncryptionSecret); - await this.RemoveEnterpriseManagedApiKeysAsync(); + await this.RemoveEnterpriseManagedSecretsAsync(); await PluginFactory.LoadAll(); } @@ -249,34 +249,36 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe return serverUrl.Trim().TrimEnd('/'); } - private async Task RemoveEnterpriseManagedApiKeysAsync() + private async Task RemoveEnterpriseManagedSecretsAsync() { var secretTargets = GetEnterpriseManagedSecretTargets(); if (secretTargets.Count == 0) { - logger.LogInformation("No enterprise-managed API keys are currently known in the settings. No keyring cleanup is required."); + logger.LogInformation("No enterprise-managed secrets are currently known in the settings. No keyring cleanup is required."); return; } - logger.LogInformation("Removing {SecretCount} enterprise-managed API key(s) from the OS keyring after an enterprise encryption secret change.", secretTargets.Count); + logger.LogInformation("Removing {SecretCount} enterprise-managed secret(s) from the OS keyring after an enterprise encryption secret change.", secretTargets.Count); foreach (var target in secretTargets) { try { - var deleteResult = await rustService.DeleteAPIKey(target, target.StoreType); + var deleteResult = target.StoreType is SecretStoreType.DATA_SOURCE + ? await rustService.DeleteSecret(target, target.StoreType) + : await rustService.DeleteAPIKey(target, target.StoreType); if (deleteResult.Success) { if (deleteResult.WasEntryFound) - logger.LogInformation("Successfully deleted enterprise-managed API key '{SecretName}' from the OS keyring.", target.SecretName); + logger.LogInformation("Successfully deleted enterprise-managed secret '{SecretName}' from the OS keyring.", target.SecretName); else - logger.LogInformation("Enterprise-managed API key '{SecretName}' was already absent from the OS keyring.", target.SecretName); + logger.LogInformation("Enterprise-managed secret '{SecretName}' was already absent from the OS keyring.", target.SecretName); } else - logger.LogWarning("Failed to delete enterprise-managed API key '{SecretName}' from the OS keyring: {Issue}", target.SecretName, deleteResult.Issue); + logger.LogWarning("Failed to delete enterprise-managed secret '{SecretName}' from the OS keyring: {Issue}", target.SecretName, deleteResult.Issue); } catch (Exception e) { - logger.LogWarning(e, "Failed to delete enterprise-managed API key '{SecretName}' from the OS keyring.", target.SecretName); + logger.LogWarning(e, "Failed to delete enterprise-managed secret '{SecretName}' from the OS keyring.", target.SecretName); } } } @@ -289,6 +291,7 @@ public sealed class EnterpriseEnvironmentService(ILogger<EnterpriseEnvironmentSe AddEnterpriseManagedSecretTargets(configurationData.Providers, SecretStoreType.LLM_PROVIDER, secretTargets); AddEnterpriseManagedSecretTargets(configurationData.EmbeddingProviders, SecretStoreType.EMBEDDING_PROVIDER, secretTargets); AddEnterpriseManagedSecretTargets(configurationData.TranscriptionProviders, SecretStoreType.TRANSCRIPTION_PROVIDER, secretTargets); + AddEnterpriseManagedSecretTargets(configurationData.DataSources.OfType<IExternalDataSource>(), SecretStoreType.DATA_SOURCE, secretTargets); return secretTargets.ToList(); } diff --git a/app/MindWork AI Studio/Tools/Services/RustService.OS.cs b/app/MindWork AI Studio/Tools/Services/RustService.OS.cs index 0b81ccfe..9fd151e8 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.OS.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.OS.cs @@ -32,4 +32,35 @@ public sealed partial class RustService this.userLanguageLock.Release(); } } + + public async Task<string> ReadUserName(bool forceRequest = false) + { + if (!forceRequest && !string.IsNullOrWhiteSpace(this.cachedUserName)) + return this.cachedUserName; + + await this.userNameLock.WaitAsync(); + try + { + if (!forceRequest && !string.IsNullOrWhiteSpace(this.cachedUserName)) + return this.cachedUserName; + + var response = await this.http.GetAsync("/system/username"); + if (!response.IsSuccessStatusCode) + { + this.logger!.LogError($"Failed to read the user name from Rust: '{response.StatusCode}'"); + return string.Empty; + } + + var userName = (await response.Content.ReadAsStringAsync()).Trim(); + if (string.IsNullOrWhiteSpace(userName)) + return string.Empty; + + this.cachedUserName = userName; + return userName; + } + finally + { + this.userNameLock.Release(); + } + } } \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Secrets.cs b/app/MindWork AI Studio/Tools/Services/RustService.Secrets.cs index 49f51a1d..36ed6b6b 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.Secrets.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.Secrets.cs @@ -4,26 +4,34 @@ namespace AIStudio.Tools.Services; public sealed partial class RustService { + private static string SecretKey(ISecretId secretId, SecretStoreType storeType) => $"{storeType.Prefix()}::{secretId.SecretId}::{secretId.SecretName}"; + + private static string LegacySecretKey(ISecretId secretId) => $"secret::{secretId.SecretId}::{secretId.SecretName}"; + /// <summary> /// Try to get the secret data for the given secret ID. /// </summary> /// <param name="secretId">The secret ID to get the data for.</param> + /// <param name="storeType">The secret store type.</param> /// <param name="isTrying">Indicates if we are trying to get the data. In that case, we don't log errors.</param> /// <returns>The requested secret.</returns> - public async Task<RequestedSecret> GetSecret(ISecretId secretId, bool isTrying = false) + public async Task<RequestedSecret> GetSecret(ISecretId secretId, SecretStoreType storeType, bool isTrying = false) { - var secretRequest = new SelectSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, isTrying); - var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions); - if (!result.IsSuccessStatusCode) + var secretKey = SecretKey(secretId, storeType); + var secret = await this.GetSecretByKey(secretKey, isTrying || storeType is SecretStoreType.DATA_SOURCE); + if (secret.Success || storeType is not SecretStoreType.DATA_SOURCE) + return secret; + + var legacySecretKey = LegacySecretKey(secretId); + var legacySecret = await this.GetSecretByKey(legacySecretKey, isTrying: true); + if (legacySecret.Success) { - if(!isTrying) - this.logger!.LogError($"Failed to get the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'"); - return new RequestedSecret(false, new EncryptedText(string.Empty), TB("Failed to get the secret data due to an API issue.")); + this.logger!.LogDebug($"Successfully retrieved the legacy data source secret for '{legacySecretKey}'."); + return legacySecret; } - - var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions); + if (!secret.Success && !isTrying) - this.logger!.LogError($"Failed to get the secret data for secret ID '{secretId.SecretId}': '{secret.Issue}'"); + this.logger!.LogError($"Failed to get the secret data for '{secretKey}': '{secret.Issue}'"); return secret; } @@ -33,21 +41,26 @@ public sealed partial class RustService /// </summary> /// <param name="secretId">The secret ID to store the data for.</param> /// <param name="secretData">The data to store.</param> + /// <param name="storeType">The secret store type.</param> /// <returns>The store secret response.</returns> - public async Task<StoreSecretResponse> SetSecret(ISecretId secretId, string secretData) + public async Task<StoreSecretResponse> SetSecret(ISecretId secretId, string secretData, SecretStoreType storeType) { + var secretKey = SecretKey(secretId, storeType); var encryptedSecret = await this.encryptor!.Encrypt(secretData); - var request = new StoreSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, encryptedSecret); + var request = new StoreSecretRequest(secretKey, Environment.UserName, encryptedSecret); var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions); if (!result.IsSuccessStatusCode) { - this.logger!.LogError($"Failed to store the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'"); - return new StoreSecretResponse(false, TB("Failed to get the secret data due to an API issue.")); + this.logger!.LogError($"Failed to store the secret data for '{secretKey}' due to an API issue: '{result.StatusCode}'"); + return new StoreSecretResponse(false, TB("Failed to store the secret data due to an API issue.")); } var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>(this.jsonRustSerializerOptions); if (!state.Success) - this.logger!.LogError($"Failed to store the secret data for secret ID '{secretId.SecretId}': '{state.Issue}'"); + this.logger!.LogError($"Failed to store the secret data for '{secretKey}': '{state.Issue}'"); + + if (state.Success && storeType is SecretStoreType.DATA_SOURCE) + await this.DeleteSecretByKey(LegacySecretKey(secretId)); return state; } @@ -56,20 +69,48 @@ public sealed partial class RustService /// Tries to delete the secret data for the given secret ID. /// </summary> /// <param name="secretId">The secret ID to delete the data for.</param> + /// <param name="storeType">The secret store type.</param> /// <returns>The delete secret response.</returns> - public async Task<DeleteSecretResponse> DeleteSecret(ISecretId secretId) + public async Task<DeleteSecretResponse> DeleteSecret(ISecretId secretId, SecretStoreType storeType) { - var request = new SelectSecretRequest($"secret::{secretId.SecretId}::{secretId.SecretName}", Environment.UserName, false); + var deleteResult = await this.DeleteSecretByKey(SecretKey(secretId, storeType)); + if (storeType is not SecretStoreType.DATA_SOURCE || !deleteResult.Success) + return deleteResult; + + var legacyDeleteResult = await this.DeleteSecretByKey(LegacySecretKey(secretId)); + if (!legacyDeleteResult.Success) + return legacyDeleteResult; + + return deleteResult with { WasEntryFound = deleteResult.WasEntryFound || legacyDeleteResult.WasEntryFound }; + } + + private async Task<RequestedSecret> GetSecretByKey(string secretKey, bool isTrying) + { + var secretRequest = new SelectSecretRequest(secretKey, Environment.UserName, isTrying); + var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions); + if (!result.IsSuccessStatusCode) + { + if(!isTrying) + this.logger!.LogError($"Failed to get the secret data for '{secretKey}' due to an API issue: '{result.StatusCode}'"); + return new RequestedSecret(false, new EncryptedText(string.Empty), TB("Failed to get the secret data due to an API issue.")); + } + + return await result.Content.ReadFromJsonAsync<RequestedSecret>(this.jsonRustSerializerOptions); + } + + private async Task<DeleteSecretResponse> DeleteSecretByKey(string secretKey) + { + var request = new SelectSecretRequest(secretKey, Environment.UserName, false); var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions); if (!result.IsSuccessStatusCode) { - this.logger!.LogError($"Failed to delete the secret data for secret ID '{secretId.SecretId}' due to an API issue: '{result.StatusCode}'"); + this.logger!.LogError($"Failed to delete the secret data for '{secretKey}' due to an API issue: '{result.StatusCode}'"); return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = TB("Failed to delete the secret data due to an API issue.")}; } var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>(this.jsonRustSerializerOptions); if (!state.Success) - this.logger!.LogError($"Failed to delete the secret data for secret ID '{secretId.SecretId}': '{state.Issue}'"); + this.logger!.LogError($"Failed to delete the secret data for '{secretKey}': '{state.Issue}'"); return state; } diff --git a/app/MindWork AI Studio/Tools/Services/RustService.cs b/app/MindWork AI Studio/Tools/Services/RustService.cs index 9f495adb..6bcef10c 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.cs @@ -18,6 +18,7 @@ public sealed partial class RustService : BackgroundService private readonly HttpClient http; private readonly SemaphoreSlim userLanguageLock = new(1, 1); + private readonly SemaphoreSlim userNameLock = new(1, 1); private readonly JsonSerializerOptions jsonRustSerializerOptions = new() { @@ -31,6 +32,7 @@ public sealed partial class RustService : BackgroundService private ILogger<RustService>? logger; private Encryption? encryptor; private string? cachedUserLanguage; + private string? cachedUserName; private readonly string apiPort; private readonly string certificateFingerprint; @@ -91,6 +93,7 @@ public sealed partial class RustService : BackgroundService { this.http.Dispose(); this.userLanguageLock.Dispose(); + this.userNameLock.Dispose(); base.Dispose(); } diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md index 1008fe32..c2ac3cd5 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md @@ -1,5 +1,8 @@ # v26.5.5, build 240 (2026-05-xx xx:xx UTC) - Released the voice recording and transcription for all users. You no longer need to enable a preview feature to configure transcription providers, select a transcription provider, or use dictation. +- Added support for organization-managed ERI servers in configuration plugins, so admins can preconfigure external data sources for users. +- Added an export option for ERI server data sources, so admins can create configuration plugin snippets without writing the Lua code manually. +- Added the username to the information page to make organization support easier when users share their screen. - Improved the app's security foundation with major modernization of the native runtime and its internal communication layer. This work is mostly invisible during everyday use, but it replaces older components that no longer received the security updates we require. We also continued updating security-sensitive dependencies so AI Studio stays on a healthier, better maintained base. - Improved the Pandoc management and detection process to make it more reliable. - Fixed the Pandoc installation, which could fail and prevent AI Studio from installing its local Pandoc dependency. diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock index c8894cac..ed6866a7 100644 --- a/runtime/Cargo.lock +++ b/runtime/Cargo.lock @@ -2893,9 +2893,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.183" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libdbus-sys" @@ -2928,11 +2928,10 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.3" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ - "bitflags 2.11.1", "libc", ] @@ -3097,6 +3096,7 @@ dependencies = [ "tempfile", "tokio", "tokio-stream", + "whoami", "windows-native-keyring-store", "windows-registry", ] @@ -3549,6 +3549,15 @@ dependencies = [ "objc2-foundation 0.3.2", ] +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "objc2-core-foundation", +] + [[package]] name = "objc2-ui-kit" version = "0.3.0" @@ -6079,6 +6088,15 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" +dependencies = [ + "wasi 0.13.3+wasi-0.2.2", +] + [[package]] name = "wasm-bindgen" version = "0.2.120" @@ -6298,6 +6316,19 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" +[[package]] +name = "whoami" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" +dependencies = [ + "libc", + "libredox", + "objc2-system-configuration", + "wasite", + "web-sys", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 304d0332..3578b14f 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -41,6 +41,7 @@ file-format = "0.29.0" calamine = "0.35.0" pdfium-render = "0.9.1" sys-locale = "0.3.2" +whoami = "2.1.2" cfg-if = "1.0.4" pptx-to-md = "0.4.0" tempfile = "3.27.0" diff --git a/runtime/src/environment.rs b/runtime/src/environment.rs index 1e45b5f3..3f8dd43c 100644 --- a/runtime/src/environment.rs +++ b/runtime/src/environment.rs @@ -1,6 +1,6 @@ use crate::api_token::APIToken; use axum::Json; -use log::{debug, info, warn}; +use log::{debug, error, info, warn}; use serde::Serialize; use std::collections::{HashMap, HashSet}; use std::env; @@ -43,6 +43,14 @@ pub async fn get_data_directory(_token: APIToken) -> String { } } +/// Returns the current user's username. +pub async fn read_user_name(_token: APIToken) -> String { + whoami::username().unwrap_or_else(|e| { + error!("Failed to read the current OS username: {e}."); + String::new() + }) +} + /// Returns true if the application is running in development mode. pub fn is_dev() -> bool { cfg!(debug_assertions) diff --git a/runtime/src/runtime_api.rs b/runtime/src/runtime_api.rs index 213c8a55..89f6cec0 100644 --- a/runtime/src/runtime_api.rs +++ b/runtime/src/runtime_api.rs @@ -48,6 +48,7 @@ pub fn start_runtime_api() { .route("/system/directories/config", get(crate::environment::get_config_directory)) .route("/system/directories/data", get(crate::environment::get_data_directory)) .route("/system/language", get(crate::environment::read_user_language)) + .route("/system/username", get(crate::environment::read_user_name)) .route("/system/enterprise/config/id", get(crate::environment::read_enterprise_env_config_id)) .route("/system/enterprise/config/server", get(crate::environment::read_enterprise_env_config_server_url)) .route("/system/enterprise/config/encryption_secret", get(crate::environment::read_enterprise_env_config_encryption_secret)) From cad7a98e7bcdf3757c9335864de48001c58a7761 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Mon, 18 May 2026 19:05:29 +0200 Subject: [PATCH 19/21] Fixed the missed spellchecking settings for the slide builder assistant (#768) --- .../Assistants/SlideBuilder/SlideAssistant.razor | 2 +- app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor b/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor index 513b335d..e451ab3d 100644 --- a/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor +++ b/app/MindWork AI Studio/Assistants/SlideBuilder/SlideAssistant.razor @@ -22,7 +22,7 @@ <MudJustifiedText Typo="Typo.body1" Class="mb-2"> @T("You might want to specify important aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted.") </MudJustifiedText> -<MudTextField T="string" AutoGrow="true" Lines="3" @bind-Text="@this.importantAspects" class="mb-1" Label="@T("(Optional) Important Aspects")" HelperText="@T("(Optional) Specify aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted.")" ShrinkLabel="true" Variant="Variant.Outlined" AdornmentIcon="@Icons.Material.Filled.List" Adornment="Adornment.Start"/> +<MudTextField T="string" AutoGrow="true" Lines="3" @bind-Text="@this.importantAspects" class="mb-1" Label="@T("(Optional) Important Aspects")" HelperText="@T("(Optional) Specify aspects that the LLM should consider when creating the slides. For example, the use of emojis or specific topics that should be highlighted.")" ShrinkLabel="true" Variant="Variant.Outlined" AdornmentIcon="@Icons.Material.Filled.List" Adornment="Adornment.Start" UserAttributes="@USER_INPUT_ATTRIBUTES"/> <MudText Typo="Typo.h6" Class="mb-1 mt-3"> @T("Extent of the planned presentation")</MudText> <MudJustifiedText Typo="Typo.body1" Class="mb-2"> diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md index c2ac3cd5..a556a5c4 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md @@ -6,6 +6,7 @@ - Improved the app's security foundation with major modernization of the native runtime and its internal communication layer. This work is mostly invisible during everyday use, but it replaces older components that no longer received the security updates we require. We also continued updating security-sensitive dependencies so AI Studio stays on a healthier, better maintained base. - Improved the Pandoc management and detection process to make it more reliable. - Fixed the Pandoc installation, which could fail and prevent AI Studio from installing its local Pandoc dependency. +- Fixed an issue where the spellchecking setting was not applied to all text fields in the slide builder assistant. - Upgraded the native secret storage integration to `keyring-core`, keeping API keys in the secure credential store provided by the operating system. - Upgraded Rust to v1.95.0. - Upgraded .NET to v9.0.16. From 97e60036864850723754cfa28de02daab0452a1d Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Mon, 18 May 2026 19:24:07 +0200 Subject: [PATCH 20/21] Fixed missing translations for file type names (#769) --- app/MindWork AI Studio/Tools/Rust/FileTypes.cs | 2 +- app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs index 87a551b2..ebd62c5e 100644 --- a/app/MindWork AI Studio/Tools/Rust/FileTypes.cs +++ b/app/MindWork AI Studio/Tools/Rust/FileTypes.cs @@ -9,7 +9,7 @@ namespace AIStudio.Tools.Rust; /// </summary> public static class FileTypes { - private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(FileTypeFilter).Namespace, nameof(FileTypeFilter)); + private static string TB(string fallbackEn) => I18N.I.T(fallbackEn, typeof(FileTypes).Namespace, nameof(FileTypes)); public static readonly FileTypeFilter SOURCE_LIKE_FILE_NAMES = FileTypeFilter.Leaf(TB("Source like"), "Dockerfile", "Containerfile", "Jenkinsfile", "Makefile", "GNUmakefile", "Procfile", "Vagrantfile", diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md index a556a5c4..682be0e0 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md @@ -7,6 +7,7 @@ - Improved the Pandoc management and detection process to make it more reliable. - Fixed the Pandoc installation, which could fail and prevent AI Studio from installing its local Pandoc dependency. - Fixed an issue where the spellchecking setting was not applied to all text fields in the slide builder assistant. +- Fixed missing translations for file type names in file selection dialogs. - Upgraded the native secret storage integration to `keyring-core`, keeping API keys in the secure credential store provided by the operating system. - Upgraded Rust to v1.95.0. - Upgraded .NET to v9.0.16. From cef1c9976581857ef06aa1d10c2b2be9b5d1228f Mon Sep 17 00:00:00 2001 From: Thorsten Sommer <SommerEngineering@users.noreply.github.com> Date: Tue, 19 May 2026 08:24:22 +0200 Subject: [PATCH 21/21] Improved Qdrant server startup & client initialization (#770) --- .../Assistants/I18N/allTexts.lua | 9 + .../Pages/Information.razor.cs | 99 +++++++++- .../plugin.lua | 9 + .../plugin.lua | 9 + app/MindWork AI Studio/Program.cs | 54 +----- .../Tools/Databases/DatabaseClient.cs | 6 +- .../Tools/Databases/DatabaseClientProvider.cs | 180 ++++++++++++++++++ .../Tools/Databases/DatabaseClientStatus.cs | 8 + .../Tools/Databases/DatabaseRole.cs | 6 + .../Tools/Databases/NoDatabaseClient.cs | 10 +- .../Qdrant/QdrantClientImplementation.cs | 7 + .../Tools/Rust/QdrantInfo.cs | 2 + .../Tools/Rust/QdrantStatus.cs | 8 + .../Tools/Services/RustService.Databases.cs | 28 ++- .../wwwroot/changelog/v26.5.5.md | 1 + runtime/src/app_window.rs | 3 +- runtime/src/qdrant.rs | 83 +++++++- 17 files changed, 443 insertions(+), 79 deletions(-) create mode 100644 app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs create mode 100644 app/MindWork AI Studio/Tools/Databases/DatabaseClientStatus.cs create mode 100644 app/MindWork AI Studio/Tools/Databases/DatabaseRole.cs create mode 100644 app/MindWork AI Studio/Tools/Rust/QdrantStatus.cs diff --git a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua index a4205982..0828bcbc 100644 --- a/app/MindWork AI Studio/Assistants/I18N/allTexts.lua +++ b/app/MindWork AI Studio/Assistants/I18N/allTexts.lua @@ -6157,6 +6157,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840227993"] = "Used .NET runtim -- Explanation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Explanation" +-- checking availability +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2855535668"] = "checking availability" + -- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software." @@ -6265,6 +6268,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "This is a library -- Used .NET SDK UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Used .NET SDK" +-- starting +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T594602073"] = "starting" + -- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated." @@ -6901,6 +6907,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = " -- Reason UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason" +-- Starting +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1233211769"] = "Starting" + -- Unavailable UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable" diff --git a/app/MindWork AI Studio/Pages/Information.razor.cs b/app/MindWork AI Studio/Pages/Information.razor.cs index 10a6b614..9f7250ac 100644 --- a/app/MindWork AI Studio/Pages/Information.razor.cs +++ b/app/MindWork AI Studio/Pages/Information.razor.cs @@ -29,7 +29,7 @@ public partial class Information : MSGComponentBase private ISnackbar Snackbar { get; init; } = null!; [Inject] - private DatabaseClient DatabaseClient { get; init; } = null!; + private DatabaseClientProvider DatabaseClientProvider { get; init; } = null!; private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly(); private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute<MetaDataAttribute>()!; @@ -62,9 +62,21 @@ public partial class Information : MSGComponentBase private string VersionPdfium => $"{T("Used PDFium version")}: v{META_DATA_LIBRARIES.PdfiumVersion}"; - private string VersionDatabase => this.DatabaseClient.IsAvailable - ? $"{T("Database version")}: {this.DatabaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}" - : $"{T("Database")}: {this.DatabaseClient.Name} - {T("not available")}"; + private string VersionDatabase + { + get + { + if (this.databaseClient is null) + return $"{T("Database")}: {T("checking availability")}"; + + return this.databaseClient.Status switch + { + DatabaseClientStatus.AVAILABLE => $"{T("Database version")}: {this.databaseClient.Name} v{META_DATA_DATABASES.DatabaseVersion}", + DatabaseClientStatus.STARTING => $"{T("Database")}: {this.databaseClient.Name} - {T("starting")}", + _ => $"{T("Database")}: {this.databaseClient.Name} - {T("not available")}" + }; + } + } private string versionPandoc = TB("Determine Pandoc version, please wait..."); private PandocInstallation pandocInstallation; @@ -89,6 +101,8 @@ public partial class Information : MSGComponentBase private sealed record MandatoryInfoPanelData(string HeaderText, string PluginName, DataMandatoryInfo Info, DataMandatoryInfoAcceptance? Acceptance); private readonly List<DatabaseDisplayInfo> databaseDisplayInfo = new(); + private DatabaseClient? databaseClient; + private CancellationTokenSource? databaseRefreshCancellationTokenSource; private bool HasAnyActiveEnvironment => this.enterpriseEnvironments.Any(e => e.IsActive); @@ -134,10 +148,9 @@ public partial class Information : MSGComponentBase this.osUserName = await this.RustService.ReadUserName(); this.logPaths = await this.RustService.GetLogPaths(); - await foreach (var (label, value) in this.DatabaseClient.GetDisplayInfo()) - { - this.databaseDisplayInfo.Add(new DatabaseDisplayInfo(label, value)); - } + await this.RefreshDatabaseInfo(CancellationToken.None); + if (this.databaseClient?.Status is DatabaseClientStatus.STARTING) + this.StartShortDatabaseRefreshLoop(); // Determine the Pandoc version may take some time, so we start it here // without waiting for the result: @@ -241,6 +254,69 @@ public partial class Information : MSGComponentBase this.showDatabaseDetails = !this.showDatabaseDetails; } + private async Task RefreshDatabaseInfo(CancellationToken cancellationToken) + { + var refreshedClient = await this.DatabaseClientProvider.RefreshClientAsync(DatabaseRole.VECTOR_STORE, cancellationToken); + this.databaseClient = refreshedClient; + this.databaseDisplayInfo.Clear(); + + try + { + await foreach (var (label, value) in refreshedClient.GetDisplayInfo().WithCancellation(cancellationToken)) + { + this.databaseDisplayInfo.Add(new DatabaseDisplayInfo(label, value)); + } + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception e) + { + this.databaseClient = new NoDatabaseClient(refreshedClient.Name, e.Message, DatabaseClientStatus.STARTING); + await foreach (var (label, value) in this.databaseClient.GetDisplayInfo().WithCancellation(cancellationToken)) + { + this.databaseDisplayInfo.Add(new DatabaseDisplayInfo(label, value)); + } + } + } + + private void StartShortDatabaseRefreshLoop() + { + this.databaseRefreshCancellationTokenSource?.Cancel(); + this.databaseRefreshCancellationTokenSource?.Dispose(); + this.databaseRefreshCancellationTokenSource = new CancellationTokenSource(); + var cancellationToken = this.databaseRefreshCancellationTokenSource.Token; + + _ = Task.Run(async () => + { + const int MAX_TRIES = 12; + for (var attempt = 0; attempt < MAX_TRIES; attempt++) + { + try + { + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); + await this.InvokeAsync(async () => + { + await this.RefreshDatabaseInfo(cancellationToken); + this.StateHasChanged(); + }); + + if (this.databaseClient?.Status is not DatabaseClientStatus.STARTING) + return; + } + catch (OperationCanceledException) + { + return; + } + catch + { + return; + } + } + }, cancellationToken); + } + private IAvailablePlugin? FindManagedConfigurationPlugin(Guid configurationId) { return this.configPlugins.FirstOrDefault(plugin => plugin.ManagedConfigurationId == configurationId) @@ -253,6 +329,13 @@ public partial class Information : MSGComponentBase return plugin.ManagedConfigurationId == configurationId && plugin.Id != configurationId; } + protected override void DisposeResources() + { + this.databaseRefreshCancellationTokenSource?.Cancel(); + this.databaseRefreshCancellationTokenSource?.Dispose(); + base.DisposeResources(); + } + private async Task CopyStartupLogPath() { await this.RustService.CopyText2Clipboard(this.Snackbar, this.logPaths.LogStartupPath); diff --git a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua index adcba747..f499a093 100644 --- a/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/de-de-43065dbc-78d0-45b7-92be-f14c2926e2dc/plugin.lua @@ -6159,6 +6159,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840227993"] = "Verwendete .NET- -- Explanation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Erklärung" +-- checking availability +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2855535668"] = "Verfügbarkeit wird geprüft" + -- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "Das .NET-Backend kann nicht als Desktop-App gestartet werden. Deshalb verwende ich ein zweites Backend in Rust, das ich „Runtime“ nenne. Mit Rust als Runtime kann Tauri genutzt werden, um eine typische Desktop-App zu realisieren. Dank Rust kann diese App für Windows-, macOS- und Linux-Desktops angeboten werden. Rust ist eine großartige Sprache für die Entwicklung sicherer und leistungsstarker Software." @@ -6267,6 +6270,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "Dies ist eine Bib -- Used .NET SDK UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Verwendetes .NET SDK" +-- starting +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T594602073"] = "wird gestartet" + -- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "Diese Bibliothek wird verwendet, um Sidecar-Prozesse zu verwalten und sicherzustellen, dass veraltete oder Zombie-Sidecars erkannt und beendet werden." @@ -6903,6 +6909,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = " -- Reason UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Grund" +-- Starting +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1233211769"] = "Wird gestartet" + -- Unavailable UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Nicht verfügbar" diff --git a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua index 0f5389cf..3726cd6b 100644 --- a/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua +++ b/app/MindWork AI Studio/Plugins/languages/en-us-97dfb1ba-50c4-4440-8dfa-6575daf543c8/plugin.lua @@ -6159,6 +6159,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840227993"] = "Used .NET runtim -- Explanation UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2840582448"] = "Explanation" +-- checking availability +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2855535668"] = "checking availability" + -- The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T2868174483"] = "The .NET backend cannot be started as a desktop app. Therefore, I use a second backend in Rust, which I call runtime. With Rust as the runtime, Tauri can be used to realize a typical desktop app. Thanks to Rust, this app can be offered for Windows, macOS, and Linux desktops. Rust is a great language for developing safe and high-performance software." @@ -6267,6 +6270,9 @@ UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T566998575"] = "This is a library -- Used .NET SDK UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T585329785"] = "Used .NET SDK" +-- starting +UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T594602073"] = "starting" + -- This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated. UI_TEXT_CONTENT["AISTUDIO::PAGES::INFORMATION::T633932150"] = "This library is used to manage sidecar processes and to ensure that stale or zombie sidecars are detected and terminated." @@ -6903,6 +6909,9 @@ UI_TEXT_CONTENT["AISTUDIO::TOOLS::CONFIDENCESCHEMESEXTENSIONS::T4107860491"] = " -- Reason UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1093747001"] = "Reason" +-- Starting +UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T1233211769"] = "Starting" + -- Unavailable UI_TEXT_CONTENT["AISTUDIO::TOOLS::DATABASES::NODATABASECLIENT::T3662391977"] = "Unavailable" diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs index f2b9b06c..996c5c43 100644 --- a/app/MindWork AI Studio/Program.cs +++ b/app/MindWork AI Studio/Program.cs @@ -2,7 +2,6 @@ using AIStudio.Agents; using AIStudio.Agents.AssistantAudit; using AIStudio.Settings; using AIStudio.Tools.Databases; -using AIStudio.Tools.Databases.Qdrant; using AIStudio.Tools.PluginSystem; using AIStudio.Tools.PluginSystem.Assistants; using AIStudio.Tools.Services; @@ -28,7 +27,7 @@ internal sealed class Program public static string API_TOKEN = null!; public static IServiceProvider SERVICE_PROVIDER = null!; public static ILoggerFactory LOGGER_FACTORY = null!; - public static DatabaseClient DATABASE_CLIENT = null!; + public static DatabaseClientProvider DATABASE_CLIENT_PROVIDER = null!; public static async Task Main() { @@ -87,48 +86,6 @@ internal sealed class Program return; } - var qdrantInfo = await rust.GetQdrantInfo(); - DatabaseClient databaseClient; - if (!qdrantInfo.IsAvailable) - { - Console.WriteLine($"Warning: Qdrant is not available. Starting without vector database. Reason: '{qdrantInfo.UnavailableReason ?? "unknown"}'."); - databaseClient = new NoDatabaseClient("Qdrant", qdrantInfo.UnavailableReason); - } - else - { - if (qdrantInfo.Path == string.Empty) - { - Console.WriteLine("Error: Failed to get the Qdrant path from Rust."); - return; - } - - if (qdrantInfo.PortHttp == 0) - { - Console.WriteLine("Error: Failed to get the Qdrant HTTP port from Rust."); - return; - } - - if (qdrantInfo.PortGrpc == 0) - { - Console.WriteLine("Error: Failed to get the Qdrant gRPC port from Rust."); - return; - } - - if (qdrantInfo.Fingerprint == string.Empty) - { - Console.WriteLine("Error: Failed to get the Qdrant fingerprint from Rust."); - return; - } - - if (qdrantInfo.ApiToken == string.Empty) - { - Console.WriteLine("Error: Failed to get the Qdrant API token from Rust."); - return; - } - - databaseClient = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken); - } - var builder = WebApplication.CreateBuilder(); builder.WebHost.ConfigureKestrel(kestrelServerOptions => { @@ -183,7 +140,7 @@ internal sealed class Program builder.Services.AddHostedService<UpdateService>(); builder.Services.AddHostedService<TemporaryChatService>(); builder.Services.AddHostedService<EnterpriseEnvironmentService>(); - builder.Services.AddSingleton(databaseClient); + builder.Services.AddSingleton<DatabaseClientProvider>(); builder.Services.AddHostedService<GlobalShortcutService>(); builder.Services.AddHostedService<RustAvailabilityMonitorService>(); @@ -242,10 +199,7 @@ internal sealed class Program RUST_SERVICE = rust; ENCRYPTION = encryption; - - var databaseLogger = app.Services.GetRequiredService<ILogger<DatabaseClient>>(); - databaseClient.SetLogger(databaseLogger); - DATABASE_CLIENT = databaseClient; + DATABASE_CLIENT_PROVIDER = app.Services.GetRequiredService<DatabaseClientProvider>(); programLogger.LogInformation("Initialize internal file system."); app.Use(Redirect.HandlerContentAsync); @@ -283,7 +237,7 @@ internal sealed class Program await serverTask; RUST_SERVICE.Dispose(); - DATABASE_CLIENT.Dispose(); + DATABASE_CLIENT_PROVIDER.Dispose(); PluginFactory.Dispose(); programLogger.LogInformation("The AI Studio server was stopped."); } diff --git a/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs index b80cba94..2fb9fced 100644 --- a/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs +++ b/app/MindWork AI Studio/Tools/Databases/DatabaseClient.cs @@ -4,7 +4,11 @@ public abstract class DatabaseClient(string name, string path) { public string Name => name; - public virtual bool IsAvailable => true; + public virtual string CacheKey => name; + + public virtual DatabaseClientStatus Status => DatabaseClientStatus.AVAILABLE; + + public bool IsAvailable => this.Status is DatabaseClientStatus.AVAILABLE; private string Path => path; diff --git a/app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs b/app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs new file mode 100644 index 00000000..4296ec53 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/DatabaseClientProvider.cs @@ -0,0 +1,180 @@ +using AIStudio.Tools.Databases.Qdrant; +using AIStudio.Tools.Rust; +using AIStudio.Tools.Services; + +namespace AIStudio.Tools.Databases; + +public sealed class DatabaseClientProvider(RustService rustService, ILoggerFactory loggerFactory) : IDisposable +{ + private readonly Dictionary<DatabaseRole, DatabaseClient> clients = new(); + private readonly Dictionary<DatabaseRole, SemaphoreSlim> locks = new(); + private readonly Lock locksLock = new(); + private readonly ILogger<DatabaseClientProvider> logger = loggerFactory.CreateLogger<DatabaseClientProvider>(); + private readonly ILogger<DatabaseClient> databaseClientLogger = loggerFactory.CreateLogger<DatabaseClient>(); + + public async Task<DatabaseClient> GetClientAsync(DatabaseRole databaseRole, CancellationToken cancellationToken = default) + { + var databaseLock = this.GetLock(databaseRole); + await databaseLock.WaitAsync(cancellationToken); + try + { + if (this.clients.TryGetValue(databaseRole, out var cachedClient) && cachedClient.IsAvailable) + return cachedClient; + + var client = await this.CreateClientAsync(databaseRole, cancellationToken); + return this.CacheIfAvailable(databaseRole, client); + } + finally + { + databaseLock.Release(); + } + } + + public async Task<DatabaseClient> RefreshClientAsync(DatabaseRole databaseRole, CancellationToken cancellationToken = default) + { + var databaseLock = this.GetLock(databaseRole); + await databaseLock.WaitAsync(cancellationToken); + try + { + var client = await this.CreateClientAsync(databaseRole, cancellationToken); + return this.CacheIfAvailable(databaseRole, client); + } + finally + { + databaseLock.Release(); + } + } + + private DatabaseClient CacheIfAvailable(DatabaseRole databaseRole, DatabaseClient client) + { + if (!client.IsAvailable) + return client; + + if (this.clients.TryGetValue(databaseRole, out var cachedClient)) + { + if (IsSameClient(cachedClient, client)) + { + client.Dispose(); + return cachedClient; + } + + cachedClient.Dispose(); + } + + this.clients[databaseRole] = client; + return client; + } + + private SemaphoreSlim GetLock(DatabaseRole databaseRole) + { + lock (this.locksLock) + { + if (this.locks.TryGetValue(databaseRole, out var databaseLock)) + return databaseLock; + + databaseLock = new SemaphoreSlim(1, 1); + this.locks[databaseRole] = databaseLock; + return databaseLock; + } + } + + private async Task<DatabaseClient> CreateClientAsync(DatabaseRole databaseRole, CancellationToken cancellationToken) => databaseRole switch + { + DatabaseRole.VECTOR_STORE => await this.CreateQdrantClientAsync(cancellationToken), + _ => new NoDatabaseClient(databaseRole.ToString(), "The requested database role is not supported.") + }; + + private async Task<DatabaseClient> CreateQdrantClientAsync(CancellationToken cancellationToken) + { + var qdrantInfo = await rustService.GetQdrantInfo(cancellationToken); + if (qdrantInfo.Status is QdrantStatus.STARTING) + { + return this.CreateNoDatabaseClient( + "Qdrant", + "Qdrant is starting. Details will appear shortly.", + DatabaseClientStatus.STARTING); + } + + if (!qdrantInfo.IsAvailable || qdrantInfo.Status is QdrantStatus.UNAVAILABLE) + { + var reason = qdrantInfo.UnavailableReason ?? "unknown"; + this.logger.LogWarning("Qdrant is not available. Starting without vector database. Reason: '{Reason}'.", reason); + return this.CreateNoDatabaseClient("Qdrant", qdrantInfo.UnavailableReason, DatabaseClientStatus.UNAVAILABLE); + } + + if (!HasValidQdrantConnectionInfo(qdrantInfo, out var invalidReason)) + return this.CreateNoDatabaseClient("Qdrant", invalidReason, DatabaseClientStatus.UNAVAILABLE); + + var client = new QdrantClientImplementation("Qdrant", qdrantInfo.Path, qdrantInfo.PortHttp, qdrantInfo.PortGrpc, qdrantInfo.Fingerprint, qdrantInfo.ApiToken); + client.SetLogger(this.databaseClientLogger); + + try + { + await client.CheckAvailabilityAsync(); + return client; + } + catch (Exception e) + { + client.Dispose(); + this.logger.LogWarning(e, "Qdrant reported as available by Rust, but the health check failed."); + return this.CreateNoDatabaseClient("Qdrant", e.Message, DatabaseClientStatus.STARTING); + } + } + + private static bool HasValidQdrantConnectionInfo(QdrantInfo qdrantInfo, out string invalidReason) + { + if (qdrantInfo.Path == string.Empty) + { + invalidReason = "Failed to get the Qdrant path from Rust."; + return false; + } + + if (qdrantInfo.PortHttp == 0) + { + invalidReason = "Failed to get the Qdrant HTTP port from Rust."; + return false; + } + + if (qdrantInfo.PortGrpc == 0) + { + invalidReason = "Failed to get the Qdrant gRPC port from Rust."; + return false; + } + + if (qdrantInfo.Fingerprint == string.Empty) + { + invalidReason = "Failed to get the Qdrant fingerprint from Rust."; + return false; + } + + if (qdrantInfo.ApiToken == string.Empty) + { + invalidReason = "Failed to get the Qdrant API token from Rust."; + return false; + } + + invalidReason = string.Empty; + return true; + } + + private NoDatabaseClient CreateNoDatabaseClient(string name, string? unavailableReason, DatabaseClientStatus status) + { + var client = new NoDatabaseClient(name, unavailableReason, status); + client.SetLogger(this.databaseClientLogger); + return client; + } + + private static bool IsSameClient(DatabaseClient left, DatabaseClient right) => + left.IsAvailable + && right.IsAvailable + && left.CacheKey == right.CacheKey; + + public void Dispose() + { + foreach (var client in this.clients.Values) + client.Dispose(); + + foreach (var databaseLock in this.locks.Values) + databaseLock.Dispose(); + } +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Databases/DatabaseClientStatus.cs b/app/MindWork AI Studio/Tools/Databases/DatabaseClientStatus.cs new file mode 100644 index 00000000..c9084353 --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/DatabaseClientStatus.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Tools.Databases; + +public enum DatabaseClientStatus +{ + STARTING, + AVAILABLE, + UNAVAILABLE, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Databases/DatabaseRole.cs b/app/MindWork AI Studio/Tools/Databases/DatabaseRole.cs new file mode 100644 index 00000000..d4b5be3c --- /dev/null +++ b/app/MindWork AI Studio/Tools/Databases/DatabaseRole.cs @@ -0,0 +1,6 @@ +namespace AIStudio.Tools.Databases; + +public enum DatabaseRole +{ + VECTOR_STORE, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs b/app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs index 7b3b0cd4..cd778f7b 100644 --- a/app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs +++ b/app/MindWork AI Studio/Tools/Databases/NoDatabaseClient.cs @@ -2,15 +2,19 @@ using AIStudio.Tools.PluginSystem; namespace AIStudio.Tools.Databases; -public sealed class NoDatabaseClient(string name, string? unavailableReason) : DatabaseClient(name, string.Empty) +public sealed class NoDatabaseClient(string name, string? unavailableReason, DatabaseClientStatus status = DatabaseClientStatus.UNAVAILABLE) : DatabaseClient(name, string.Empty) { private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(NoDatabaseClient).Namespace, nameof(NoDatabaseClient)); - public override bool IsAvailable => false; + public override DatabaseClientStatus Status => status; public override async IAsyncEnumerable<(string Label, string Value)> GetDisplayInfo() { - yield return (TB("Status"), TB("Unavailable")); + yield return (TB("Status"), status switch + { + DatabaseClientStatus.STARTING => TB("Starting"), + _ => TB("Unavailable") + }); if (!string.IsNullOrWhiteSpace(unavailableReason)) yield return (TB("Reason"), unavailableReason); diff --git a/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs index 60a13419..b3a09e68 100644 --- a/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs +++ b/app/MindWork AI Studio/Tools/Databases/Qdrant/QdrantClientImplementation.cs @@ -26,6 +26,8 @@ public class QdrantClientImplementation : DatabaseClient this.ApiToken = apiToken; this.GrpcClient = this.CreateQdrantClient(); } + + public override string CacheKey => $"{this.Name}:{this.HttpPort}:{this.GrpcPort}:{this.Fingerprint}"; private const string IP_ADDRESS = "localhost"; @@ -47,6 +49,11 @@ public class QdrantClientImplementation : DatabaseClient return $"v{operation.Version}"; } + public async Task CheckAvailabilityAsync() + { + await this.GrpcClient.HealthAsync(); + } + private async Task<string> GetCollectionsAmount() { var operation = await this.GrpcClient.ListCollectionsAsync(); diff --git a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs index 5315eca7..30044596 100644 --- a/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs +++ b/app/MindWork AI Studio/Tools/Rust/QdrantInfo.cs @@ -5,6 +5,8 @@ /// </summary> public readonly record struct QdrantInfo { + public QdrantStatus Status { get; init; } + public bool IsAvailable { get; init; } public string? UnavailableReason { get; init; } diff --git a/app/MindWork AI Studio/Tools/Rust/QdrantStatus.cs b/app/MindWork AI Studio/Tools/Rust/QdrantStatus.cs new file mode 100644 index 00000000..10d6246a --- /dev/null +++ b/app/MindWork AI Studio/Tools/Rust/QdrantStatus.cs @@ -0,0 +1,8 @@ +namespace AIStudio.Tools.Rust; + +public enum QdrantStatus +{ + STARTING, + AVAILABLE, + UNAVAILABLE, +} \ No newline at end of file diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs b/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs index a43f6c61..3efc8050 100644 --- a/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs +++ b/app/MindWork AI Studio/Tools/Services/RustService.Databases.cs @@ -4,13 +4,27 @@ namespace AIStudio.Tools.Services; public sealed partial class RustService { - public async Task<QdrantInfo> GetQdrantInfo() + public async Task<QdrantInfo> GetQdrantInfo(CancellationToken cancellationToken = default) { try { - var cts = new CancellationTokenSource(TimeSpan.FromSeconds(45)); - var response = await this.http.GetFromJsonAsync<QdrantInfo>("/system/qdrant/info", this.jsonRustSerializerOptions, cts.Token); - return response; + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + cts.CancelAfter(TimeSpan.FromSeconds(45)); + + return await this.http.GetFromJsonAsync<QdrantInfo>("/system/qdrant/info", this.jsonRustSerializerOptions, cts.Token); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + if(this.logger is not null) + this.logger.LogWarning("Fetching Qdrant info from Rust service was cancelled by caller."); + else + Console.WriteLine("Fetching Qdrant info from Rust service was cancelled by caller."); + + return new QdrantInfo + { + Status = QdrantStatus.UNAVAILABLE, + UnavailableReason = "Operation cancelled by caller." + }; } catch (Exception e) { @@ -19,7 +33,11 @@ public sealed partial class RustService else Console.WriteLine($"Error while fetching Qdrant info from Rust service: '{e}'."); - return default; + return new QdrantInfo + { + Status = QdrantStatus.UNAVAILABLE, + UnavailableReason = e.Message + }; } } } \ No newline at end of file diff --git a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md index 682be0e0..b8d5f9a1 100644 --- a/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md +++ b/app/MindWork AI Studio/wwwroot/changelog/v26.5.5.md @@ -5,6 +5,7 @@ - Added the username to the information page to make organization support easier when users share their screen. - Improved the app's security foundation with major modernization of the native runtime and its internal communication layer. This work is mostly invisible during everyday use, but it replaces older components that no longer received the security updates we require. We also continued updating security-sensitive dependencies so AI Studio stays on a healthier, better maintained base. - Improved the Pandoc management and detection process to make it more reliable. +- Improved the Qdrant startup and vector database initialization, so AI Studio can start more reliably while the local vector database is still starting. - Fixed the Pandoc installation, which could fail and prevent AI Studio from installing its local Pandoc dependency. - Fixed an issue where the spellchecking setting was not applied to all text fields in the slide builder assistant. - Fixed missing translations for file type names in file selection dialogs. diff --git a/runtime/src/app_window.rs b/runtime/src/app_window.rs index b52be5a5..dd54e205 100644 --- a/runtime/src/app_window.rs +++ b/runtime/src/app_window.rs @@ -25,7 +25,7 @@ use crate::dotnet::{cleanup_dotnet_server, start_dotnet_server, stop_dotnet_serv use crate::environment::{is_prod, is_dev, CONFIG_DIRECTORY, DATA_DIRECTORY}; use crate::log::switch_to_file_logging; use crate::pdfium::PDFIUM_LIB_PATH; -use crate::qdrant::{cleanup_qdrant, start_qdrant_server, stop_qdrant_server}; +use crate::qdrant::{start_qdrant_server, stop_qdrant_server}; #[cfg(debug_assertions)] use crate::dotnet::create_startup_env_file; @@ -148,7 +148,6 @@ pub fn start_tauri() { start_dotnet_server(app.handle().clone()); } - cleanup_qdrant(); start_qdrant_server(app.handle().clone()); info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {data_path:?}"); diff --git a/runtime/src/qdrant.rs b/runtime/src/qdrant.rs index 2ec9d1e9..639dd7c7 100644 --- a/runtime/src/qdrant.rs +++ b/runtime/src/qdrant.rs @@ -5,6 +5,7 @@ use std::fs::File; use std::io::Write; use std::path::Path; use std::sync::{Arc, Mutex, OnceLock}; +use std::time::Duration; use log::{debug, error, info, warn}; use once_cell::sync::Lazy; use axum::Json; @@ -18,6 +19,7 @@ use tauri::path::BaseDirectory; use tempfile::{TempDir, Builder}; use crate::stale_process_cleanup::{kill_stale_process, log_potential_stale_process}; use crate::sidecar_types::SidecarType; +use tokio::time; use tauri_plugin_shell::process::{CommandChild, CommandEvent}; use tauri_plugin_shell::ShellExt; @@ -40,14 +42,24 @@ static API_TOKEN: Lazy<APIToken> = Lazy::new(|| { }); static TMPDIR: Lazy<Mutex<Option<TempDir>>> = Lazy::new(|| Mutex::new(None)); -static QDRANT_STATUS: Lazy<Mutex<QdrantStatus>> = Lazy::new(|| Mutex::new(QdrantStatus::default())); +static QDRANT_STATUS: Lazy<Mutex<QdrantStatusInfo>> = Lazy::new(|| Mutex::new(QdrantStatusInfo::default())); const PID_FILE_NAME: &str = "qdrant.pid"; const SIDECAR_TYPE:SidecarType = SidecarType::Qdrant; +const STARTUP_TIMEOUT: Duration = Duration::from_secs(60); +const STARTUP_CHECK_INTERVAL: Duration = Duration::from_millis(250); + +#[derive(Clone, Copy, Default, Serialize, PartialEq, Eq)] +enum QdrantStatus { + #[default] + Starting, + Available, + Unavailable, +} #[derive(Default)] -struct QdrantStatus { - is_available: bool, +struct QdrantStatusInfo { + status: QdrantStatus, unavailable_reason: Option<String>, } @@ -60,6 +72,7 @@ fn qdrant_base_path() -> PathBuf { #[derive(Serialize)] pub struct ProvideQdrantInfo { + status: QdrantStatus, path: String, port_http: u16, port_grpc: u16, @@ -71,10 +84,12 @@ pub struct ProvideQdrantInfo { pub async fn qdrant_port(_token: APIToken) -> Json<ProvideQdrantInfo> { let status = QDRANT_STATUS.lock().unwrap(); - let is_available = status.is_available; + let current_status = status.status; + let is_available = current_status == QdrantStatus::Available; let unavailable_reason = status.unavailable_reason.clone(); Json(ProvideQdrantInfo { + status: current_status, path: if is_available { qdrant_base_path().to_string_lossy().to_string() } else { @@ -99,6 +114,14 @@ pub async fn qdrant_port(_token: APIToken) -> Json<ProvideQdrantInfo> { /// Starts the Qdrant server in a separate process. pub fn start_qdrant_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>){ + set_qdrant_starting(); + tauri::async_runtime::spawn(async move { + cleanup_qdrant(); + start_qdrant_server_internal(app_handle); + }); +} + +fn start_qdrant_server_internal<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>){ let path = qdrant_base_path(); if !path.exists() && let Err(e) = fs::create_dir_all(&path){ error!(Source="Qdrant"; "The required directory to host the Qdrant database could not be created: {}", e); @@ -117,12 +140,13 @@ pub fn start_qdrant_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>){ let storage_path = path.join("storage").to_string_lossy().to_string(); let snapshot_path = path.join("snapshots").to_string_lossy().to_string(); - let init_path = path.join(".qdrant-initialized").to_string_lossy().to_string(); + let init_path = path.join(".qdrant-initialized"); + let init_path_environment = init_path.to_string_lossy().to_string(); let qdrant_server_environment: HashMap<String, String> = HashMap::from_iter([ (String::from("QDRANT__SERVICE__HTTP_PORT"), QDRANT_SERVER_PORT_HTTP.to_string()), (String::from("QDRANT__SERVICE__GRPC_PORT"), QDRANT_SERVER_PORT_GRPC.to_string()), - (String::from("QDRANT_INIT_FILE_PATH"), init_path), + (String::from("QDRANT_INIT_FILE_PATH"), init_path_environment), (String::from("QDRANT__STORAGE__STORAGE_PATH"), storage_path), (String::from("QDRANT__STORAGE__SNAPSHOTS_PATH"), snapshot_path), (String::from("QDRANT__TLS__CERT"), cert_path.to_string_lossy().to_string()), @@ -172,13 +196,24 @@ pub fn start_qdrant_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>){ }; let server_pid = child.pid(); - set_qdrant_available(); info!(Source = "Bootloader Qdrant"; "Qdrant server process started with PID={server_pid}."); log_potential_stale_process(path.join(PID_FILE_NAME), server_pid, SIDECAR_TYPE); // Save the server process to stop it later: *server_spawn_clone.lock().unwrap() = Some(child); + let init_path_clone = init_path.clone(); + tauri::async_runtime::spawn(async move { + if wait_for_qdrant_startup(init_path_clone).await { + set_qdrant_available(); + info!(Source = "Qdrant"; "Qdrant is available."); + } else { + let reason = "Qdrant did not become available within the startup timeout.".to_string(); + error!(Source = "Qdrant"; "{reason}"); + set_qdrant_unavailable(reason); + } + }); + // Log the output of the Qdrant server: while let Some(event) = rx.recv().await { match event { @@ -200,10 +235,18 @@ pub fn start_qdrant_server<R: tauri::Runtime>(app_handle: tauri::AppHandle<R>){ let line_utf8 = String::from_utf8_lossy(&line).to_string(); error!(Source = "Qdrant Server (stderr)"; "{line_utf8}"); }, - + _ => {} } } + + let is_available = QDRANT_STATUS.lock().unwrap().status == QdrantStatus::Available; + let unavailable_reason = if is_available { + "Qdrant server process stopped.".to_string() + } else { + "Qdrant server process stopped before it became available.".to_string() + }; + set_qdrant_unavailable(unavailable_reason); }); } @@ -226,6 +269,20 @@ pub fn stop_qdrant_server() { cleanup_qdrant(); } +async fn wait_for_qdrant_startup(init_path: PathBuf) -> bool { + let mut elapsed = Duration::ZERO; + while elapsed < STARTUP_TIMEOUT { + if init_path.exists() { + return true; + } + + time::sleep(STARTUP_CHECK_INTERVAL).await; + elapsed += STARTUP_CHECK_INTERVAL; + } + + false +} + /// Create a temporary directory with TLS relevant files pub fn create_temp_tls_files(path: &PathBuf) -> Result<(PathBuf, PathBuf), Box<dyn Error>> { let cert = generate_certificate(); @@ -278,13 +335,19 @@ pub fn cleanup_qdrant() { fn set_qdrant_available() { let mut status = QDRANT_STATUS.lock().unwrap(); - status.is_available = true; + status.status = QdrantStatus::Available; + status.unavailable_reason = None; +} + +fn set_qdrant_starting() { + let mut status = QDRANT_STATUS.lock().unwrap(); + status.status = QdrantStatus::Starting; status.unavailable_reason = None; } fn set_qdrant_unavailable(reason: String) { let mut status = QDRANT_STATUS.lock().unwrap(); - status.is_available = false; + status.status = QdrantStatus::Unavailable; status.unavailable_reason = Some(reason); }