diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc03c43..dd7e578 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,13 +9,7 @@ on: - 'package.json' - 'src-tauri/Cargo.toml' - 'src-tauri/tauri.conf.json' - pull_request: - branches: - - main - paths: - - 'package.json' - - 'src-tauri/Cargo.toml' - - 'src-tauri/tauri.conf.json' + jobs: # Check if version actually changed diff --git a/.gitignore b/.gitignore index a0cc46d..4024f98 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ dist dist-ssr *.local +.claude/ + # Editor directories and files .vscode/* !.vscode/extensions.json diff --git a/README.md b/README.md index 92644e1..847ac9a 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,16 @@ ChatMe combines agent-driven AI, cross-platform responsiveness, and voice interaction into a single desktop/web app, empowering developers and users to interact with AI like never before. ChatMe supports multiple AI providers with a beautiful, responsive interface and advanced features including voice interaction and powerful agent mode with full system access. -## 🆕 **What's New in v0.4.0** +## 🆕 **What's New in v0.5.3** +- **⚡ Hybrid Architecture**: Revolutionary change - Rust backend handles all HTTP requests, completely bypassing CORS +- **🌐 13 Provider Support**: Added 5+ new providers (Kimi, OpenRouter, Together AI, Groq, Perplexity) - now supporting 13 total! +- **📡 Native Streaming**: Provider-specific implementations for OpenAI, Anthropic, and Google with optimal performance +- **🎨 Enhanced UX**: Improved delete button flow, welcome screen auto-send, and smoother interactions +- **🔧 Bug Fixes**: Fixed tool parameter mismatch and various UX improvements +- **🚀 Better Reliability**: Improved error handling, streaming compatibility, and overall stability +- **🌐 MCP support**: Added MCP support for seamless integration with other tools + +### **Previous Release - v0.4.0** - **🚀 Enhanced Agent Mode**: Execute terminal commands, launch apps, and manage processes - **📊 Command Execution Cards**: Interactive, expandable results with timing and copy buttons - **âŒ¨ī¸ Keyboard Shortcuts**: Ctrl+K to focus, Ctrl+R to repeat last command @@ -55,12 +64,20 @@ ChatMe combines agent-driven AI, cross-platform responsiveness, and voice intera ## ✨ Features -### đŸŽ¯ **Multi-Provider AI Support** +### đŸŽ¯ **Multi-Provider AI Support (13 Providers)** - **OpenAI**: GPT-4, GPT-4 Turbo, GPT-3.5 Turbo and many more -- **Google Gemini**: Gemini Pro, Gemini 1.5 Flash/Pro and many more (both original and OpenAI-compatible APIs) -- **Anthropic Claude**: Claude 3 models +- **Anthropic Claude**: Claude 3/3.5 Opus, Sonnet, Haiku (native streaming) +- **Google Gemini**: Gemini Pro, Gemini 1.5 Flash/Pro and many more (native streaming) +- **Mistral AI**: Mistral Large, Mixtral 8x7B, and more +- **DeepSeek**: DeepSeek Chat and Coder models +- **LMStudio**: Local LLM inference server - **Ollama**: Local models (Llama 2, CodeLlama, Mistral, etc.) -- **Custom APIs**: Support for Mistral AI, Groq, Together AI, Perplexity, and any OpenAI-compatible API +- **Kimi**: Moonshot AI models +- **OpenRouter**: Unified API for multiple providers +- **Together AI**: Open-source models at scale +- **Groq**: Ultra-fast LLM inference +- **Perplexity**: Search-enhanced AI responses +- **Custom APIs**: Any OpenAI-compatible endpoint ### 🤖 **Enhanced Agent Mode (NEW!)** - **Full System Access**: Execute terminal commands, launch applications, and manage files @@ -288,22 +305,26 @@ While native mobile apps are in development, you can test the responsive UI: - **Sonner**: Toast notifications - **React Markdown**: Rich text rendering with custom components -### Backend -- **Rust**: High-performance backend +### Backend (Hybrid Architecture) +- **Rust**: High-performance backend handling all HTTP requests - **SQLite**: Local database storage with migrations -- **Reqwest**: HTTP client for API calls +- **Reqwest**: HTTP client for LLM API calls (bypasses CORS) +- **Server-Sent Events**: Native streaming for all providers - **Tauri Commands**: File system operations and agent capabilities - **Serde**: JSON serialization - **Tokio**: Async runtime +- **Frontend LLM Client**: TypeScript tool calling logic using official SDKs patterns -### Features +### Features & Architecture +- **Hybrid Architecture**: Rust handles HTTP (no CORS issues), frontend handles tool calling - **Real-time Events**: Tauri event system for streaming - **Speech Integration**: Web Speech API for voice input/output - **Agent System**: Intelligent file operations with LLM-driven commands -- **Streaming Support**: Server-sent events simulation for real-time responses +- **Provider-Specific Streaming**: Native implementations for OpenAI, Anthropic, and Google - **Error Handling**: Comprehensive error management with user-friendly messages - **Theme System**: Dark/light mode switching with system preference detection - **Custom Rendering**: Enhanced markdown with interactive file components +- **Tool Calling**: Multi-turn conversations with automatic tool execution ## 📁 Project Structure diff --git a/images/dark.png b/images/dark.png index 120e61e..6a38880 100644 Binary files a/images/dark.png and b/images/dark.png differ diff --git a/images/light.png b/images/light.png index ea6c349..5db167f 100644 Binary files a/images/light.png and b/images/light.png differ diff --git a/package-lock.json b/package-lock.json index 45fbaf1..29fd037 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "chatme", - "version": "0.4.0", + "version": "0.5.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "chatme", - "version": "0.4.0", + "version": "0.5.3", "dependencies": { "@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-collapsible": "^1.1.12", @@ -20,8 +20,8 @@ "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.12", - "@tauri-apps/api": "^2.8.0", - "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/api": "^2.10.0", + "@tauri-apps/plugin-opener": "^2.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.542.0", @@ -2223,9 +2223,9 @@ } }, "node_modules/@tauri-apps/api": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.8.0.tgz", - "integrity": "sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==", + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.10.1.tgz", + "integrity": "sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==", "license": "Apache-2.0 OR MIT", "funding": { "type": "opencollective", diff --git a/package.json b/package.json index cac998e..d38bb92 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "chatme", "private": true, - "version": "0.4.0", + "version": "0.5.3", "type": "module", "scripts": { "dev": "vite", @@ -32,8 +32,8 @@ "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/typography": "^0.5.16", "@tailwindcss/vite": "^4.1.12", - "@tauri-apps/api": "^2.8.0", - "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/api": "^2.10.0", + "@tauri-apps/plugin-opener": "^2.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.542.0", @@ -61,4 +61,4 @@ "typescript": "~5.8.3", "vite": "^7.0.4" } -} +} \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5c48d76..76551b9 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2,15 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "addr2line" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" -dependencies = [ - "gimli", -] - [[package]] name = "adler2" version = "2.0.1" @@ -19,9 +10,9 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -58,9 +49,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" [[package]] name = "async-broadcast" @@ -88,9 +79,9 @@ dependencies = [ [[package]] name = "async-executor" -version = "1.13.3" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" dependencies = [ "async-task", "concurrent-queue", @@ -107,7 +98,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ "autocfg", - "cfg-if 1.0.3", + "cfg-if 1.0.4", "concurrent-queue", "futures-io", "futures-lite", @@ -115,14 +106,14 @@ dependencies = [ "polling", "rustix", "slab", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] name = "async-lock" -version = "3.4.1" +version = "3.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" dependencies = [ "event-listener", "event-listener-strategy", @@ -141,7 +132,7 @@ dependencies = [ "async-signal", "async-task", "blocking", - "cfg-if 1.0.3", + "cfg-if 1.0.4", "event-listener", "futures-lite", "rustix", @@ -155,7 +146,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -167,13 +158,13 @@ dependencies = [ "async-io", "async-lock", "atomic-waker", - "cfg-if 1.0.3", + "cfg-if 1.0.4", "futures-core", "futures-io", "rustix", "signal-hook-registry", "slab", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -190,7 +181,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -237,21 +228,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "backtrace" -version = "0.3.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" -dependencies = [ - "addr2line", - "cfg-if 1.0.3", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-targets 0.52.6", -] - [[package]] name = "base64" version = "0.21.7" @@ -266,9 +242,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.8.0" +version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bitflags" @@ -278,11 +254,11 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -296,20 +272,11 @@ dependencies = [ [[package]] name = "block2" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" -dependencies = [ - "objc2 0.5.2", -] - -[[package]] -name = "block2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" dependencies = [ - "objc2 0.6.2", + "objc2", ] [[package]] @@ -348,9 +315,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", "regex-automata", @@ -359,15 +326,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.0" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytemuck" -version = "1.23.2" +version = "1.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" [[package]] name = "byteorder" @@ -387,9 +354,9 @@ dependencies = [ [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" dependencies = [ "serde", ] @@ -400,7 +367,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "cairo-sys-rs", "glib", "libc", @@ -421,9 +388,9 @@ dependencies = [ [[package]] name = "camino" -version = "1.2.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1de8bc0aa9e9385ceb3bf0c152e3a9b9544f6c4a912c8ae504e80c1f0368603" +checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48" dependencies = [ "serde_core", ] @@ -448,7 +415,7 @@ dependencies = [ "semver", "serde", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.18", ] [[package]] @@ -458,14 +425,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" dependencies = [ "serde", - "toml 0.9.5", + "toml 0.9.12+spec-1.1.0", ] [[package]] name = "cc" -version = "1.2.37" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "shlex", @@ -506,19 +473,13 @@ checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" [[package]] name = "cfg-if" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chatme" -version = "0.4.0" +version = "0.5.3" dependencies = [ "anyhow", "chrono", @@ -527,7 +488,7 @@ dependencies = [ "mime_guess", "opener", "regex", - "reqwest", + "reqwest 0.12.28", "serde", "serde_json", "sqlx", @@ -543,16 +504,16 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.42" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -561,7 +522,7 @@ version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.1", "memchr", ] @@ -624,11 +585,11 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" -version = "0.24.0" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "core-foundation 0.10.1", "core-graphics-types", "foreign-types 0.5.0", @@ -641,7 +602,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "core-foundation 0.10.1", "libc", ] @@ -657,9 +618,9 @@ dependencies = [ [[package]] name = "crc" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" dependencies = [ "crc-catalog", ] @@ -676,7 +637,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ - "cfg-if 1.0.3", + "cfg-if 1.0.4", ] [[package]] @@ -753,9 +714,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", "typenum", @@ -785,7 +746,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -795,14 +756,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501" dependencies = [ "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "darling" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" dependencies = [ "darling_core", "darling_macro", @@ -810,34 +771,34 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "dbus" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9" +checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" dependencies = [ "libc", "libdbus-sys", @@ -857,12 +818,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.3" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -875,7 +836,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -929,23 +890,19 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.2", - "windows-sys 0.61.0", + "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" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", + "bitflags 2.11.0", + "block2", + "libc", + "objc2", ] [[package]] @@ -956,14 +913,14 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "dlopen2" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b54f373ccf864bf587a89e880fb7610f8d73f3045f13580948ccbcaff26febff" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" dependencies = [ "dlopen2_derive", "libc", @@ -973,13 +930,13 @@ dependencies = [ [[package]] name = "dlopen2_derive" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -999,9 +956,9 @@ dependencies = [ [[package]] name = "dtoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" [[package]] name = "dtoa-short" @@ -1035,14 +992,14 @@ dependencies = [ [[package]] name = "embed-resource" -version = "3.0.5" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6d81016d6c977deefb2ef8d8290da019e27cc26167e102185da528e6c0ab38" +checksum = "55a075fc573c64510038d7ee9abc7990635863992f83ebc52c8b433b8411a02e" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.5", + "toml 0.9.12+spec-1.1.0", "vswhom", "winreg", ] @@ -1059,14 +1016,14 @@ version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ - "cfg-if 1.0.3", + "cfg-if 1.0.4", ] [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enumflags2" @@ -1086,7 +1043,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1097,9 +1054,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "erased-serde" -version = "0.4.8" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" dependencies = [ "serde", "serde_core", @@ -1113,7 +1070,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -1122,7 +1079,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" dependencies = [ - "cfg-if 1.0.3", + "cfg-if 1.0.4", "home", "windows-sys 0.48.0", ] @@ -1175,15 +1132,15 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.1" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -1239,7 +1196,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1281,9 +1238,9 @@ checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -1291,15 +1248,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -1319,9 +1276,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-lite" @@ -1338,32 +1295,32 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-io", @@ -1372,7 +1329,6 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] @@ -1500,39 +1456,46 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if 1.0.3", + "cfg-if 1.0.4", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ - "cfg-if 1.0.3", + "cfg-if 1.0.4", "libc", "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ - "cfg-if 1.0.3", + "cfg-if 1.0.4", "libc", - "r-efi", - "wasi 0.14.5+wasi-0.2.4", + "r-efi 5.3.0", + "wasip2", ] [[package]] -name = "gimli" -version = "0.31.1" +name = "getrandom" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if 1.0.4", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] [[package]] name = "gio" @@ -1572,7 +1535,7 @@ version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "futures-channel", "futures-core", "futures-executor", @@ -1596,11 +1559,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" dependencies = [ "heck 0.4.1", - "proc-macro-crate 2.0.0", + "proc-macro-crate 2.0.2", "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -1679,22 +1642,22 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "h2" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" dependencies = [ "atomic-waker", - "bytes 1.10.1", + "bytes 1.11.1", "fnv", "futures-core", "futures-sink", "http", - "indexmap 2.11.1", + "indexmap 2.13.0", "slab", "tokio", "tokio-util", @@ -1718,6 +1681,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "hashlink" version = "0.10.0" @@ -1771,11 +1740,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.11" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1792,12 +1761,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ - "bytes 1.10.1", - "fnv", + "bytes 1.11.1", "itoa", ] @@ -1807,7 +1775,7 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.1", "http", ] @@ -1817,7 +1785,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.1", "futures-core", "http", "http-body", @@ -1832,12 +1800,12 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" dependencies = [ "atomic-waker", - "bytes 1.10.1", + "bytes 1.11.1", "futures-channel", "futures-core", "h2", @@ -1874,7 +1842,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.1", "http-body-util", "hyper", "hyper-util", @@ -1886,14 +1854,13 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ "base64 0.22.1", - "bytes 1.10.1", + "bytes 1.11.1", "futures-channel", - "futures-core", "futures-util", "http", "http-body", @@ -1912,9 +1879,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.64" +version = "0.1.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1922,7 +1889,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.0", + "windows-core 0.62.2", ] [[package]] @@ -1936,9 +1903,9 @@ 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", @@ -1946,9 +1913,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" dependencies = [ "displaydoc", "potential_utf", @@ -1959,9 +1926,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" dependencies = [ "displaydoc", "litemap", @@ -1972,11 +1939,10 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" dependencies = [ - "displaydoc", "icu_collections", "icu_normalizer_data", "icu_properties", @@ -1987,42 +1953,38 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" [[package]] name = "icu_properties" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" dependencies = [ - "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", "icu_provider", - "potential_utf", "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "2.0.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" [[package]] name = "icu_provider" -version = "2.0.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" dependencies = [ "displaydoc", "icu_locale_core", - "stable_deref_trait", - "tinystr", "writeable", "yoke", "zerofrom", @@ -2030,6 +1992,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + [[package]] name = "ident_case" version = "1.0.1" @@ -2070,13 +2038,14 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206a8042aec68fa4a62e8d3f7aa4ceb508177d9324faf261e1959e495b7a1921" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.15.5", + "hashbrown 0.16.1", "serde", + "serde_core", ] [[package]] @@ -2088,17 +2057,6 @@ dependencies = [ "cfb", ] -[[package]] -name = "io-uring" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" -dependencies = [ - "bitflags 2.9.4", - "cfg-if 1.0.3", - "libc", -] - [[package]] name = "iovec" version = "0.1.4" @@ -2110,15 +2068,15 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.11.0" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" dependencies = [ "memchr", "serde", @@ -2145,9 +2103,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "javascriptcore-rs" @@ -2179,7 +2137,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", - "cfg-if 1.0.3", + "cfg-if 1.0.4", "combine", "jni-sys", "log", @@ -2196,9 +2154,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.78" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0b063578492ceec17683ef2f8c5e89121fbd0b172cbc280635ab7567db2738" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -2232,7 +2190,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "serde", "unicode-segmentation", ] @@ -2245,7 +2203,7 @@ checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2" dependencies = [ "cssparser", "html5ever", - "indexmap 2.11.1", + "indexmap 2.13.0", "selectors", ] @@ -2258,6 +2216,12 @@ dependencies = [ "spin", ] +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libappindicator" version = "0.9.0" @@ -2284,15 +2248,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.175" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libdbus-sys" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" dependencies = [ "cc", "pkg-config", @@ -2304,25 +2268,26 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ - "cfg-if 1.0.3", + "cfg-if 1.0.4", "winapi", ] [[package]] name = "libm" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.10" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "libc", - "redox_syscall", + "plain", + "redox_syscall 0.7.3", ] [[package]] @@ -2338,31 +2303,30 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "mac" @@ -2392,7 +2356,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -2413,15 +2377,15 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ - "cfg-if 1.0.3", + "cfg-if 1.0.4", "digest", ] [[package]] name = "memchr" -version = "2.7.5" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "memoffset" @@ -2469,13 +2433,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -2488,22 +2452,22 @@ dependencies = [ "dpi", "gtk", "keyboard-types", - "objc2 0.6.2", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation", "once_cell", "png", "serde", - "thiserror 2.0.16", + "thiserror 2.0.18", "windows-sys 0.60.2", ] [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", @@ -2522,7 +2486,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "jni-sys", "log", "ndk-sys", @@ -2552,19 +2516,6 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.9.4", - "cfg-if 1.0.3", - "cfg_aliases", - "libc", - "memoffset 0.9.1", -] - [[package]] name = "nodrop" version = "0.1.14" @@ -2573,20 +2524,19 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] name = "normpath" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c178369371fd7db523726931e50d430b560e3059665abc537ba3277e9274c9c4" +checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] name = "num-bigint-dig" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" dependencies = [ - "byteorder", "lazy_static", "libm", "num-integer", @@ -2599,9 +2549,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-integer" @@ -2645,9 +2595,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" dependencies = [ "num_enum_derive", "rustversion", @@ -2655,37 +2605,21 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ - "proc-macro-crate 3.3.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.106", -] - -[[package]] -name = "objc-sys" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" - -[[package]] -name = "objc2" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" -dependencies = [ - "objc-sys", - "objc2-encode", + "syn 2.0.117", ] [[package]] name = "objc2" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" dependencies = [ "objc2-encode", "objc2-exception-helper", @@ -2693,79 +2627,41 @@ dependencies = [ [[package]] name = "objc2-app-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", - "libc", - "objc2 0.6.2", - "objc2-cloud-kit", - "objc2-core-data", + "bitflags 2.11.0", + "block2", + "objc2", "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image", - "objc2-foundation 0.3.1", - "objc2-quartz-core 0.3.1", -] - -[[package]] -name = "objc2-cloud-kit" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-foundation 0.3.1", -] - -[[package]] -name = "objc2-core-data" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-foundation 0.3.1", + "objc2-foundation", ] [[package]] name = "objc2-core-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "dispatch2", - "objc2 0.6.2", + "objc2", ] [[package]] name = "objc2-core-graphics" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "dispatch2", - "objc2 0.6.2", + "objc2", "objc2-core-foundation", "objc2-io-surface", ] -[[package]] -name = "objc2-core-image" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e" -dependencies = [ - "objc2 0.6.2", - "objc2-foundation 0.3.1", -] - [[package]] name = "objc2-encode" version = "4.1.0" @@ -2783,132 +2679,63 @@ dependencies = [ [[package]] name = "objc2-foundation" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" -dependencies = [ - "bitflags 2.9.4", - "block2 0.5.1", - "libc", - "objc2 0.5.2", -] - -[[package]] -name = "objc2-foundation" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", - "libc", - "objc2 0.6.2", + "bitflags 2.11.0", + "block2", + "objc2", "objc2-core-foundation", ] [[package]] name = "objc2-io-surface" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-javascript-core" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9052cb1bb50a4c161d934befcf879526fb87ae9a68858f241e693ca46225cf5a" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" dependencies = [ - "objc2 0.6.2", + "bitflags 2.11.0", + "objc2", "objc2-core-foundation", ] -[[package]] -name = "objc2-metal" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" -dependencies = [ - "bitflags 2.9.4", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", -] - -[[package]] -name = "objc2-quartz-core" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" -dependencies = [ - "bitflags 2.9.4", - "block2 0.5.1", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-metal", -] - [[package]] name = "objc2-quartz-core" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5" -dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", - "objc2-foundation 0.3.1", -] - -[[package]] -name = "objc2-security" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1f8e0ef3ab66b08c42644dcb34dba6ec0a574bbd8adbb8bdbdc7a2779731a44" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", + "bitflags 2.11.0", + "objc2", "objc2-core-foundation", + "objc2-foundation", ] [[package]] name = "objc2-ui-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ - "bitflags 2.9.4", - "objc2 0.6.2", + "bitflags 2.11.0", + "objc2", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation", ] [[package]] name = "objc2-web-kit" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", - "objc2 0.6.2", + "bitflags 2.11.0", + "block2", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.1", - "objc2-javascript-core", - "objc2-security", -] - -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "memchr", + "objc2-foundation", ] [[package]] @@ -2919,9 +2746,9 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "open" -version = "5.3.2" +version = "5.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" dependencies = [ "dunce", "is-wsl", @@ -2943,12 +2770,12 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.73" +version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.9.4", - "cfg-if 1.0.3", + "bitflags 2.11.0", + "cfg-if 1.0.4", "foreign-types 0.3.2", "libc", "once_cell", @@ -2964,20 +2791,20 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "openssl-probe" -version = "0.1.6" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.109" +version = "0.9.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", @@ -3034,9 +2861,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -3044,15 +2871,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ - "cfg-if 1.0.3", + "cfg-if 1.0.4", "libc", - "redox_syscall", + "redox_syscall 0.5.18", "smallvec", - "windows-targets 0.52.6", + "windows-link 0.2.1", ] [[package]] @@ -3180,7 +3007,7 @@ dependencies = [ "phf_shared 0.11.3", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3207,14 +3034,14 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" dependencies = [ - "siphasher 1.0.1", + "siphasher 1.0.2", ] [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "pin-utils" @@ -3224,9 +3051,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" dependencies = [ "atomic-waker", "fastrand", @@ -3260,14 +3087,20 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + [[package]] name = "plist" -version = "1.7.4" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap 2.11.1", + "indexmap 2.13.0", "quick-xml", "serde", "time", @@ -3292,19 +3125,19 @@ version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ - "cfg-if 1.0.3", + "cfg-if 1.0.4", "concurrent-queue", "hermit-abi", "pin-project-lite", "rustix", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] name = "potential_utf" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" dependencies = [ "zerovec", ] @@ -3330,6 +3163,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -3342,20 +3185,21 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" dependencies = [ - "toml_edit 0.20.7", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", ] [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.22.27", + "toml_edit 0.25.4+spec-1.1.0", ] [[package]] @@ -3390,27 +3234,27 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quick-xml" -version = "0.38.3" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -3421,6 +3265,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + [[package]] name = "rand" version = "0.7.3" @@ -3481,7 +3331,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", ] [[package]] @@ -3510,11 +3360,20 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", ] [[package]] @@ -3523,7 +3382,7 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", "thiserror 1.0.69", ] @@ -3534,36 +3393,36 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" dependencies = [ - "getrandom 0.2.16", + "getrandom 0.2.17", "libredox", - "thiserror 2.0.16", + "thiserror 2.0.18", ] [[package]] name = "ref-cast" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0ae411dbe946a674d89546582cea4ba2bb8defac896622d6496f14c23ba5cf" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.24" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "regex" -version = "1.11.2" +version = "1.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" dependencies = [ "aho-corasick", "memchr", @@ -3573,9 +3432,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -3584,18 +3443,18 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.12.23" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", - "bytes 1.10.1", + "bytes 1.11.1", "encoding_rs", "futures-core", "futures-util", @@ -3627,7 +3486,41 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.4.2", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +dependencies = [ + "base64 0.22.1", + "bytes 1.11.1", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams 0.5.0", "web-sys", ] @@ -3638,8 +3531,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", - "cfg-if 1.0.3", - "getrandom 0.2.16", + "cfg-if 1.0.4", + "getrandom 0.2.17", "libc", "untrusted", "windows-sys 0.52.0", @@ -3647,9 +3540,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.8" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ "const-oid", "digest", @@ -3665,12 +3558,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustc-demangle" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" - [[package]] name = "rustc_version" version = "0.4.1" @@ -3682,22 +3569,22 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ - "bitflags 2.9.4", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" dependencies = [ "once_cell", "ring", @@ -3709,18 +3596,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.12.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" dependencies = [ "zeroize", ] [[package]] name = "rustls-webpki" -version = "0.103.5" +version = "0.103.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a37813727b78798e53c2bec3f5e8fe12a6d6f8389bf9ca7802add4c9905ad8" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" dependencies = [ "ring", "rustls-pki-types", @@ -3735,9 +3622,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -3754,7 +3641,7 @@ version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -3786,9 +3673,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.4" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" dependencies = [ "dyn-clone", "ref-cast", @@ -3805,7 +3692,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3816,12 +3703,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.9.4", - "core-foundation 0.9.4", + "bitflags 2.11.0", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -3829,9 +3716,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", @@ -3867,9 +3754,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.223" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a505d71960adde88e293da5cb5eda57093379f64e61cf77bf0e6a63af07a7bac" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", @@ -3889,22 +3776,22 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.223" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20f57cbd357666aa7b3ac84a90b4ea328f1d4ddb6772b430caa5d9e1309bb9e9" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.223" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d428d07faf17e306e699ec1e91996e5a165ba5d6bce5b5155173e91a8a01a56" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3915,20 +3802,20 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -3939,7 +3826,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -3953,11 +3840,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" dependencies = [ - "serde", + "serde_core", ] [[package]] @@ -3974,19 +3861,18 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.14.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.11.1", + "indexmap 2.13.0", "schemars 0.9.0", - "schemars 1.0.4", - "serde", - "serde_derive", + "schemars 1.2.1", + "serde_core", "serde_json", "serde_with_macros", "time", @@ -3994,14 +3880,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.14.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -4023,7 +3909,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -4042,7 +3928,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ - "cfg-if 1.0.3", + "cfg-if 1.0.4", "cpufeatures", "digest", ] @@ -4053,7 +3939,7 @@ version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ - "cfg-if 1.0.3", + "cfg-if 1.0.4", "cpufeatures", "digest", ] @@ -4066,10 +3952,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -4085,9 +3972,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" [[package]] name = "siphasher" @@ -4097,15 +3984,15 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" [[package]] name = "siphasher" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -4118,34 +4005,34 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.0" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "softbuffer" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ "bytemuck", - "cfg_aliases", - "core-graphics", - "foreign-types 0.5.0", "js-sys", - "log", - "objc2 0.5.2", - "objc2-foundation 0.2.2", - "objc2-quartz-core 0.2.2", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", "raw-window-handle", - "redox_syscall", + "redox_syscall 0.5.18", + "tracing", "wasm-bindgen", "web-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -4213,7 +4100,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" dependencies = [ "base64 0.22.1", - "bytes 1.10.1", + "bytes 1.11.1", "chrono", "crc", "crossbeam-queue 0.3.12", @@ -4225,7 +4112,7 @@ dependencies = [ "futures-util", "hashbrown 0.15.5", "hashlink", - "indexmap 2.11.1", + "indexmap 2.13.0", "log", "memchr", "once_cell", @@ -4235,7 +4122,7 @@ dependencies = [ "serde_json", "sha2", "smallvec", - "thiserror 2.0.16", + "thiserror 2.0.18", "tokio", "tokio-stream", "tracing", @@ -4254,7 +4141,7 @@ dependencies = [ "quote", "sqlx-core", "sqlx-macros-core", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -4277,7 +4164,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.106", + "syn 2.0.117", "tokio", "url", ] @@ -4290,9 +4177,9 @@ checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.11.0", "byteorder", - "bytes 1.10.1", + "bytes 1.11.1", "chrono", "crc", "digest", @@ -4320,7 +4207,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror 2.0.18", "tracing", "uuid", "whoami", @@ -4334,7 +4221,7 @@ checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" dependencies = [ "atoi", "base64 0.22.1", - "bitflags 2.9.4", + "bitflags 2.11.0", "byteorder", "chrono", "crc", @@ -4359,7 +4246,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror 2.0.16", + "thiserror 2.0.18", "tracing", "uuid", "whoami", @@ -4385,7 +4272,7 @@ dependencies = [ "serde", "serde_urlencoded", "sqlx-core", - "thiserror 2.0.16", + "thiserror 2.0.18", "tracing", "url", "uuid", @@ -4393,15 +4280,9 @@ dependencies = [ [[package]] name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "static_assertions" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "string_cache" @@ -4475,9 +4356,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -4501,16 +4382,16 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[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.9.4", + "bitflags 2.11.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -4534,41 +4415,39 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.23", + "toml 0.8.2", "version-compare", ] [[package]] name = "tao" -version = "0.34.3" +version = "0.34.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "959469667dbcea91e5485fc48ba7dd6023face91bb0f1a14681a70f99847c3f7" +checksum = "6e06d52c379e63da659a483a958110bbde891695a0ecb53e48cc7786d5eda7bb" dependencies = [ - "bitflags 2.9.4", - "block2 0.6.1", + "bitflags 2.11.0", + "block2", "core-foundation 0.10.1", "core-graphics", "crossbeam-channel", - "dispatch", + "dispatch2", "dlopen2", "dpi", "gdkwayland-sys", "gdkx11-sys", "gtk", "jni", - "lazy_static", "libc", "log", "ndk", "ndk-context", "ndk-sys", - "objc2 0.6.2", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.1", + "objc2-foundation", "once_cell", "parking_lot", "raw-window-handle", - "scopeguard", "tao-macros", "unicode-segmentation", "url", @@ -4586,7 +4465,7 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -4597,17 +4476,17 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tauri" -version = "2.8.5" +version = "2.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d1d3b3dc4c101ac989fd7db77e045cc6d91a25349cd410455cb5c57d510c1c" +checksum = "da77cc00fb9028caf5b5d4650f75e31f1ef3693459dfca7f7e506d1ecef0ba2d" dependencies = [ "anyhow", - "bytes 1.10.1", + "bytes 1.11.1", "cookie", "dirs 6.0.0", "dunce", "embed_plist", - "getrandom 0.3.3", + "getrandom 0.3.4", "glob", "gtk", "heck 0.5.0", @@ -4617,15 +4496,15 @@ dependencies = [ "log", "mime", "muda", - "objc2 0.6.2", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.1", + "objc2-foundation", "objc2-ui-kit", "objc2-web-kit", "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "serde_repr", @@ -4636,11 +4515,10 @@ dependencies = [ "tauri-runtime", "tauri-runtime-wry", "tauri-utils", - "thiserror 2.0.16", + "thiserror 2.0.18", "tokio", "tray-icon", "url", - "urlpattern", "webkit2gtk", "webview2-com", "window-vibrancy", @@ -4649,9 +4527,9 @@ dependencies = [ [[package]] name = "tauri-build" -version = "2.4.1" +version = "2.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c432ccc9ff661803dab74c6cd78de11026a578a9307610bbc39d3c55be7943f" +checksum = "4bbc990d1dbf57a8e1c7fa2327f2a614d8b757805603c1b9ba5c81bade09fd4d" dependencies = [ "anyhow", "cargo_toml", @@ -4665,15 +4543,15 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.9.5", + "toml 0.9.12+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-codegen" -version = "2.4.0" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab3a62cf2e6253936a8b267c2e95839674e7439f104fa96ad0025e149d54d8a" +checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29" dependencies = [ "base64 0.22.1", "brotli", @@ -4687,9 +4565,9 @@ dependencies = [ "serde", "serde_json", "sha2", - "syn 2.0.106", + "syn 2.0.117", "tauri-utils", - "thiserror 2.0.16", + "thiserror 2.0.18", "time", "url", "uuid", @@ -4698,23 +4576,23 @@ dependencies = [ [[package]] name = "tauri-macros" -version = "2.4.0" +version = "2.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4368ea8094e7045217edb690f493b55b30caf9f3e61f79b4c24b6db91f07995e" +checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7" dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "tauri-codegen", "tauri-utils", ] [[package]] name = "tauri-plugin" -version = "2.4.0" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9946a3cede302eac0c6eb6c6070ac47b1768e326092d32efbb91f21ed58d978f" +checksum = "ddde7d51c907b940fb573006cdda9a642d6a7c8153657e88f8a5c3c9290cd4aa" dependencies = [ "anyhow", "glob", @@ -4723,27 +4601,27 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.9.5", + "toml 0.9.12+spec-1.1.0", "walkdir", ] [[package]] name = "tauri-plugin-opener" -version = "2.5.0" +version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786156aa8e89e03d271fbd3fe642207da8e65f3c961baa9e2930f332bf80a1f5" +checksum = "fc624469b06f59f5a29f874bbc61a2ed737c0f9c23ef09855a292c389c42e83f" dependencies = [ "dunce", "glob", "objc2-app-kit", - "objc2-foundation 0.3.1", + "objc2-foundation", "open", "schemars 0.8.22", "serde", "serde_json", "tauri", "tauri-plugin", - "thiserror 2.0.16", + "thiserror 2.0.18", "url", "windows", "zbus", @@ -4751,23 +4629,23 @@ dependencies = [ [[package]] name = "tauri-runtime" -version = "2.8.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4cfc9ad45b487d3fded5a4731a567872a4812e9552e3964161b08edabf93846" +checksum = "2826d79a3297ed08cd6ea7f412644ef58e32969504bc4fbd8d7dbeabc4445ea2" dependencies = [ "cookie", "dpi", "gtk", "http", "jni", - "objc2 0.6.2", + "objc2", "objc2-ui-kit", "objc2-web-kit", "raw-window-handle", "serde", "serde_json", "tauri-utils", - "thiserror 2.0.16", + "thiserror 2.0.18", "url", "webkit2gtk", "webview2-com", @@ -4776,17 +4654,16 @@ dependencies = [ [[package]] name = "tauri-runtime-wry" -version = "2.8.1" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1fe9d48bd122ff002064e88cfcd7027090d789c4302714e68fcccba0f4b7807" +checksum = "e11ea2e6f801d275fdd890d6c9603736012742a1c33b96d0db788c9cdebf7f9e" dependencies = [ "gtk", "http", "jni", "log", - "objc2 0.6.2", + "objc2", "objc2-app-kit", - "objc2-foundation 0.3.1", "once_cell", "percent-encoding", "raw-window-handle", @@ -4803,9 +4680,9 @@ dependencies = [ [[package]] name = "tauri-utils" -version = "2.7.0" +version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a3852fdf9a4f8fbeaa63dc3e9a85284dd6ef7200751f0bd66ceee30c93f212" +checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d" dependencies = [ "anyhow", "brotli", @@ -4831,8 +4708,8 @@ dependencies = [ "serde_json", "serde_with", "swift-rs", - "thiserror 2.0.16", - "toml 0.9.5", + "thiserror 2.0.18", + "toml 0.9.12+spec-1.1.0", "url", "urlpattern", "uuid", @@ -4841,25 +4718,26 @@ dependencies = [ [[package]] name = "tauri-winres" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd21509dd1fa9bd355dc29894a6ff10635880732396aa38c0066c1e6c1ab8074" +checksum = "1087b111fe2b005e42dbdc1990fc18593234238d47453b0c99b7de1c9ab2c1e0" dependencies = [ + "dunce", "embed-resource", - "toml 0.9.5", + "toml 0.9.12+spec-1.1.0", ] [[package]] name = "tempfile" -version = "3.22.0" +version = "3.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" dependencies = [ "fastrand", - "getrandom 0.3.3", + "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -4884,11 +4762,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.18", ] [[package]] @@ -4899,45 +4777,46 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "time" -version = "0.3.43" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bde6f1ec10e72d583d91623c939f623002284ef622b87de38cfd546cbf2031" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", + "itoa", "num-conv", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.24" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -4945,9 +4824,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" dependencies = [ "displaydoc", "zerovec", @@ -4970,22 +4849,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.47.1" +version = "1.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" dependencies = [ - "backtrace", - "bytes 1.10.1", - "io-uring", + "bytes 1.11.1", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "slab", "socket2", "tokio-macros", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -5022,13 +4898,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -5043,9 +4919,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ "rustls", "tokio", @@ -5053,9 +4929,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" dependencies = [ "futures-core", "pin-project-lite", @@ -5081,11 +4957,11 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.16" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" dependencies = [ - "bytes 1.10.1", + "bytes 1.11.1", "futures-core", "futures-sink", "pin-project-lite", @@ -5094,47 +4970,56 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.23" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", ] [[package]] name = "toml" -version = "0.9.5" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ - "indexmap 2.11.1", - "serde", - "serde_spanned 1.0.0", - "toml_datetime 0.7.0", + "indexmap 2.13.0", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", - "winnow 0.7.13", + "winnow 0.7.15", ] [[package]] name = "toml_datetime" -version = "0.6.11" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_datetime" -version = "0.7.0" +version = "0.7.5+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" dependencies = [ - "serde", + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", ] [[package]] @@ -5143,55 +5028,56 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.11.1", - "toml_datetime 0.6.11", + "indexmap 2.13.0", + "toml_datetime 0.6.3", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.20.7" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" dependencies = [ - "indexmap 2.11.1", - "toml_datetime 0.6.11", + "indexmap 2.13.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.27" +version = "0.25.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ - "indexmap 2.11.1", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "winnow 0.7.13", + "indexmap 2.13.0", + "toml_datetime 1.0.0+spec-1.1.0", + "toml_parser", + "winnow 0.7.15", ] [[package]] name = "toml_parser" -version = "1.0.2" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b551886f449aa90d4fe2bdaa9f4a2577ad2dde302c61ecf262d80b116db95c10" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ - "winnow 0.7.13", + "winnow 0.7.15", ] [[package]] name = "toml_writer" -version = "1.0.2" +version = "1.0.6+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" [[package]] name = "tower" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", @@ -5204,12 +5090,12 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ - "bitflags 2.9.4", - "bytes 1.10.1", + "bitflags 2.11.0", + "bytes 1.11.1", "futures-util", "http", "http-body", @@ -5234,9 +5120,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "log", "pin-project-lite", @@ -5246,44 +5132,44 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", ] [[package]] name = "tray-icon" -version = "0.21.1" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0d92153331e7d02ec09137538996a7786fe679c629c279e82a6be762b7e6fe2" +checksum = "a5e85aa143ceb072062fc4d6356c1b520a51d636e7bc8e77ec94be3608e5e80c" dependencies = [ "crossbeam-channel", "dirs 6.0.0", "libappindicator", "muda", - "objc2 0.6.2", + "objc2", "objc2-app-kit", "objc2-core-foundation", "objc2-core-graphics", - "objc2-foundation 0.3.1", + "objc2-foundation", "once_cell", "png", "serde", - "thiserror 2.0.16", - "windows-sys 0.59.0", + "thiserror 2.0.18", + "windows-sys 0.60.2", ] [[package]] @@ -5300,19 +5186,19 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "uds_windows" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca" dependencies = [ "memoffset 0.9.1", "tempfile", - "winapi", + "windows-sys 0.61.2", ] [[package]] @@ -5358,9 +5244,9 @@ dependencies = [ [[package]] name = "unicase" -version = "2.8.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" [[package]] name = "unicode-bidi" @@ -5370,24 +5256,24 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" [[package]] name = "unicode-ident" -version = "1.0.19" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-normalization" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" [[package]] name = "unicode-segmentation" @@ -5395,6 +5281,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.9.0" @@ -5403,14 +5295,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.7" +version = "2.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" dependencies = [ "form_urlencoded", "idna", "percent-encoding", "serde", + "serde_derive", ] [[package]] @@ -5439,13 +5332,13 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.18.1" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.4.2", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -5457,9 +5350,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version-compare" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" [[package]] name = "version_check" @@ -5519,19 +5412,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.5+wasi-0.2.4" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ - "wasip2", + "wit-bindgen", ] [[package]] -name = "wasip2" -version = "1.0.0+wasi-0.2.4" +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen", ] @@ -5544,38 +5437,25 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.101" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e14915cadd45b529bb8d1f343c4ed0ac1de926144b746e2710f9cd05df6603b" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ - "cfg-if 1.0.3", + "cfg-if 1.0.4", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.101" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28d1ba982ca7923fd01448d5c30c6864d0a14109560296a162f80f305fb93bb" -dependencies = [ - "bumpalo", - "log", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasm-bindgen-shared", -] - [[package]] name = "wasm-bindgen-futures" -version = "0.4.51" +version = "0.4.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca85039a9b469b38336411d6d6ced91f3fc87109a2a27b0c197663f5144dffe" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" dependencies = [ - "cfg-if 1.0.3", + "cfg-if 1.0.4", + "futures-util", "js-sys", "once_cell", "wasm-bindgen", @@ -5584,9 +5464,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.101" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3d463ae3eff775b0c45df9da45d68837702ac35af998361e2c84e7c5ec1b0d" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5594,26 +5474,48 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.101" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb4ce89b08211f923caf51d527662b75bdc9c9c7aab40f86dcb9fb85ac552aa" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ + "bumpalo", "proc-macro2", "quote", - "syn 2.0.106", - "wasm-bindgen-backend", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.101" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f143854a3b13752c6950862c906306adb27c7e839f7414cec8fea35beab624c1" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -5627,11 +5529,36 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver", +] + [[package]] name = "web-sys" -version = "0.3.78" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e4b637749ff0d92b8fad63aa1f7cff3cbe125fd49c175cd6345e7272638b12" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -5639,9 +5566,9 @@ dependencies = [ [[package]] name = "webkit2gtk" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76b1bc1e54c581da1e9f179d0b38512ba358fb1af2d634a1affe42e37172361a" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" dependencies = [ "bitflags 1.3.2", "cairo-rs", @@ -5663,9 +5590,9 @@ dependencies = [ [[package]] name = "webkit2gtk-sys" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62daa38afc514d1f8f12b8693d30d5993ff77ced33ce30cd04deebc267a6d57c" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" dependencies = [ "bitflags 1.3.2", "cairo-sys-rs", @@ -5687,23 +5614,23 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.2", + "webpki-roots 1.0.6", ] [[package]] name = "webpki-roots" -version = "1.0.2" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" dependencies = [ "rustls-pki-types", ] [[package]] name = "webview2-com" -version = "0.38.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ba622a989277ef3886dd5afb3e280e3dd6d974b766118950a08f8f678ad6a4" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", @@ -5715,22 +5642,22 @@ dependencies = [ [[package]] name = "webview2-com-macros" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "webview2-com-sys" -version = "0.38.0" +version = "0.38.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36695906a1b53a3bf5c4289621efedac12b73eeb0b89e7e1a89b517302d5d75c" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ - "thiserror 2.0.16", + "thiserror 2.0.18", "windows", "windows-core 0.61.2", ] @@ -5767,7 +5694,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.0", + "windows-sys 0.61.2", ] [[package]] @@ -5782,10 +5709,10 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" dependencies = [ - "objc2 0.6.2", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation", "raw-window-handle", "windows-sys 0.59.0", "windows-version", @@ -5828,15 +5755,15 @@ dependencies = [ [[package]] name = "windows-core" -version = "0.62.0" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.0", - "windows-result 0.4.0", - "windows-strings 0.5.0", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -5852,24 +5779,24 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -5880,9 +5807,9 @@ checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-numerics" @@ -5896,13 +5823,13 @@ dependencies = [ [[package]] name = "windows-registry" -version = "0.5.3" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", ] [[package]] @@ -5916,11 +5843,11 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -5934,11 +5861,11 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -5983,16 +5910,16 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.3", + "windows-targets 0.53.5", ] [[package]] name = "windows-sys" -version = "0.61.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -6043,19 +5970,19 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.3" +version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ - "windows-link 0.1.3", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", ] [[package]] @@ -6069,11 +5996,11 @@ dependencies = [ [[package]] name = "windows-version" -version = "0.1.5" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69e061eb0a22b4a1d778ad70f7575ec7845490abb35b08fa320df7895882cacb" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" dependencies = [ - "windows-link 0.2.0", + "windows-link 0.2.1", ] [[package]] @@ -6096,9 +6023,9 @@ checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" @@ -6120,9 +6047,9 @@ checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_aarch64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" @@ -6144,9 +6071,9 @@ checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" @@ -6156,9 +6083,9 @@ checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" @@ -6180,9 +6107,9 @@ checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_i686_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" @@ -6204,9 +6131,9 @@ checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnu" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" @@ -6228,9 +6155,9 @@ checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_gnullvm" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" @@ -6252,9 +6179,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "windows_x86_64_msvc" -version = "0.53.0" +version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" @@ -6267,9 +6194,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" dependencies = [ "memchr", ] @@ -6280,30 +6207,112 @@ version = "0.55.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" dependencies = [ - "cfg-if 1.0.3", + "cfg-if 1.0.4", "windows-sys 0.59.0", ] [[package]] name = "wit-bindgen" -version = "0.45.1" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] [[package]] name = "writeable" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] name = "wry" -version = "0.53.3" +version = "0.54.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f0e9642a0d061f6236c54ccae64c2722a7879ad4ec7dff59bd376d446d8e90" +checksum = "bb26159b420aa77684589a744ae9a9461a95395b848764ad12290a14d960a11a" dependencies = [ "base64 0.22.1", - "block2 0.6.1", + "block2", "cookie", "crossbeam-channel", "dirs 6.0.0", @@ -6318,10 +6327,10 @@ dependencies = [ "kuchikiki", "libc", "ndk", - "objc2 0.6.2", + "objc2", "objc2-app-kit", "objc2-core-foundation", - "objc2-foundation 0.3.1", + "objc2-foundation", "objc2-ui-kit", "objc2-web-kit", "once_cell", @@ -6330,7 +6339,7 @@ dependencies = [ "sha2", "soup3", "tao-macros", - "thiserror 2.0.16", + "thiserror 2.0.18", "url", "webkit2gtk", "webkit2gtk-sys", @@ -6364,11 +6373,10 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" dependencies = [ - "serde", "stable_deref_trait", "yoke-derive", "zerofrom", @@ -6376,21 +6384,21 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "synstructure", ] [[package]] name = "zbus" -version = "5.11.0" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" dependencies = [ "async-broadcast", "async-executor", @@ -6406,14 +6414,16 @@ dependencies = [ "futures-core", "futures-lite", "hex", - "nix", + "libc", "ordered-stream", + "rustix", "serde", "serde_repr", "tracing", "uds_windows", - "windows-sys 0.60.2", - "winnow 0.7.13", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.15", "zbus_macros", "zbus_names", "zvariant", @@ -6421,14 +6431,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.11.0" +version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" dependencies = [ - "proc-macro-crate 3.3.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "zbus_names", "zvariant", "zvariant_utils", @@ -6436,34 +6446,33 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.2.0" +version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", - "static_assertions", - "winnow 0.7.13", + "winnow 0.7.15", "zvariant", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] [[package]] @@ -6483,21 +6492,21 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "synstructure", ] [[package]] name = "zeroize" -version = "1.8.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" [[package]] name = "zerotrie" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" dependencies = [ "displaydoc", "yoke", @@ -6506,9 +6515,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.4" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" dependencies = [ "yoke", "zerofrom", @@ -6517,51 +6526,57 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", ] +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + [[package]] name = "zvariant" -version = "5.7.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" dependencies = [ "endi", "enumflags2", "serde", - "winnow 0.7.13", + "winnow 0.7.15", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "5.7.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" dependencies = [ - "proc-macro-crate 3.3.0", + "proc-macro-crate 3.5.0", "proc-macro2", "quote", - "syn 2.0.106", + "syn 2.0.117", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.2.1" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.106", - "winnow 0.7.13", + "syn 2.0.117", + "winnow 0.7.15", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f626ab1..2c2e31e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "chatme" -version = "0.4.0" +version = "0.5.3" description = "A Tauri App" authors = ["you"] edition = "2021" diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 5a7eb80..5fe54b8 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -6,7 +6,7 @@ "permissions": [ "core:default", "opener:default", - "core:window:default", + "core:window:default", "core:window:allow-start-dragging", "core:window:allow-close", "core:window:allow-minimize", diff --git a/src-tauri/migrations/004_add_new_providers.sql b/src-tauri/migrations/004_add_new_providers.sql new file mode 100644 index 0000000..7c4e290 --- /dev/null +++ b/src-tauri/migrations/004_add_new_providers.sql @@ -0,0 +1,46 @@ +-- Add new AI providers to the CHECK constraint +-- SQLite doesn't support ALTER TABLE to modify CHECK constraints directly +-- So we need to recreate the table with the new constraint + +-- Step 1: Create new table with updated constraint +CREATE TABLE api_configs_new ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + provider TEXT NOT NULL CHECK (provider IN ( + 'openai', + 'anthropic', + 'google', + 'ollama', + 'mistral', + 'deepseek', + 'lmstudio', + 'kimi', + 'openrouter', + 'together', + 'groq', + 'perplexity', + 'custom' + )), + api_key TEXT NOT NULL, + base_url TEXT, + model TEXT NOT NULL, + temperature REAL NOT NULL DEFAULT 0.7, + max_tokens INTEGER, + is_default BOOLEAN NOT NULL DEFAULT FALSE, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL +); + +-- Step 2: Copy data from old table to new table +INSERT INTO api_configs_new +SELECT * FROM api_configs; + +-- Step 3: Drop old table +DROP TABLE api_configs; + +-- Step 4: Rename new table to original name +ALTER TABLE api_configs_new RENAME TO api_configs; + +-- Step 5: Recreate indexes +CREATE INDEX IF NOT EXISTS idx_api_configs_provider ON api_configs(provider); +CREATE INDEX IF NOT EXISTS idx_api_configs_is_default ON api_configs(is_default); diff --git a/src-tauri/migrations/005_add_system_role_and_permissions_table.sql b/src-tauri/migrations/005_add_system_role_and_permissions_table.sql new file mode 100644 index 0000000..1a71136 --- /dev/null +++ b/src-tauri/migrations/005_add_system_role_and_permissions_table.sql @@ -0,0 +1,46 @@ +-- Add 'system' role support to messages table +-- SQLite doesn't support modifying CHECK constraints, so we need to recreate the table + +-- Create permission_requests table first +CREATE TABLE IF NOT EXISTS permission_requests ( + id TEXT PRIMARY KEY NOT NULL, + chat_id TEXT NOT NULL, + operation TEXT NOT NULL, + description TEXT NOT NULL, + level TEXT NOT NULL CHECK (level IN ('Safe', 'Moderate', 'Dangerous')), + details TEXT, -- JSON object with operation details + status TEXT NOT NULL CHECK (status IN ('pending', 'approved', 'denied')), + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + FOREIGN KEY (chat_id) REFERENCES chats (id) ON DELETE CASCADE +); + +-- Create new messages table with system role support and permission_request_id +CREATE TABLE IF NOT EXISTS messages_new ( + id TEXT PRIMARY KEY NOT NULL, + chat_id TEXT NOT NULL, + content TEXT NOT NULL, + role TEXT NOT NULL CHECK (role IN ('user', 'assistant', 'system')), + created_at DATETIME NOT NULL, + images TEXT, + permission_request_id TEXT, -- Foreign key to permission_requests table + FOREIGN KEY (chat_id) REFERENCES chats (id) ON DELETE CASCADE, + FOREIGN KEY (permission_request_id) REFERENCES permission_requests (id) ON DELETE CASCADE +); + +-- Copy data from old messages table +INSERT INTO messages_new (id, chat_id, content, role, created_at, images, permission_request_id) +SELECT id, chat_id, content, role, created_at, images, NULL FROM messages; + +-- Drop old messages table +DROP TABLE messages; + +-- Rename new table +ALTER TABLE messages_new RENAME TO messages; + +-- Create indexes +CREATE INDEX IF NOT EXISTS idx_messages_chat_id ON messages(chat_id); +CREATE INDEX IF NOT EXISTS idx_messages_created_at ON messages(created_at); +CREATE INDEX IF NOT EXISTS idx_messages_permission_request ON messages(permission_request_id); +CREATE INDEX IF NOT EXISTS idx_permission_requests_chat_id ON permission_requests(chat_id); +CREATE INDEX IF NOT EXISTS idx_permission_requests_status ON permission_requests(status); diff --git a/src-tauri/migrations/006_tool_executions.sql b/src-tauri/migrations/006_tool_executions.sql new file mode 100644 index 0000000..0cb258c --- /dev/null +++ b/src-tauri/migrations/006_tool_executions.sql @@ -0,0 +1,21 @@ +-- Tool executions table to persist tool call history +CREATE TABLE IF NOT EXISTS tool_executions ( + id TEXT PRIMARY KEY NOT NULL, + message_id TEXT NOT NULL, + tool_call_id TEXT NOT NULL, + tool_name TEXT NOT NULL, + tool_source TEXT NOT NULL DEFAULT 'builtin', + arguments TEXT NOT NULL, + result TEXT, + success INTEGER NOT NULL DEFAULT 0, + error_message TEXT, + execution_order INTEGER NOT NULL DEFAULT 0, + started_at DATETIME NOT NULL, + completed_at DATETIME, + FOREIGN KEY (message_id) REFERENCES messages (id) ON DELETE CASCADE +); + +-- Indexes for efficient queries +CREATE INDEX IF NOT EXISTS idx_tool_executions_message_id ON tool_executions(message_id); +CREATE INDEX IF NOT EXISTS idx_tool_executions_tool_call_id ON tool_executions(tool_call_id); +CREATE INDEX IF NOT EXISTS idx_tool_executions_tool_name ON tool_executions(tool_name); diff --git a/src-tauri/migrations/007_mcp_servers.sql b/src-tauri/migrations/007_mcp_servers.sql new file mode 100644 index 0000000..9084738 --- /dev/null +++ b/src-tauri/migrations/007_mcp_servers.sql @@ -0,0 +1,39 @@ +-- MCP server configurations +CREATE TABLE IF NOT EXISTS mcp_servers ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + transport_type TEXT NOT NULL CHECK (transport_type IN ('stdio', 'sse')), + -- For stdio transport + command TEXT, + args TEXT, -- JSON array of arguments + env TEXT, -- JSON object of environment variables + -- For SSE transport + url TEXT, + headers TEXT, -- JSON object of headers + -- Common fields + enabled INTEGER NOT NULL DEFAULT 1, + auto_connect INTEGER NOT NULL DEFAULT 1, + connection_timeout_ms INTEGER DEFAULT 30000, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL +); + +-- MCP tools discovered from servers +CREATE TABLE IF NOT EXISTS mcp_tools ( + id TEXT PRIMARY KEY NOT NULL, + server_id TEXT NOT NULL, + name TEXT NOT NULL, + description TEXT, + input_schema TEXT NOT NULL, -- JSON Schema + enabled INTEGER NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL, + updated_at DATETIME NOT NULL, + FOREIGN KEY (server_id) REFERENCES mcp_servers (id) ON DELETE CASCADE, + UNIQUE(server_id, name) +); + +-- Indexes for efficient queries +CREATE INDEX IF NOT EXISTS idx_mcp_servers_enabled ON mcp_servers(enabled); +CREATE INDEX IF NOT EXISTS idx_mcp_tools_server_id ON mcp_tools(server_id); +CREATE INDEX IF NOT EXISTS idx_mcp_tools_name ON mcp_tools(name); +CREATE INDEX IF NOT EXISTS idx_mcp_tools_enabled ON mcp_tools(enabled); diff --git a/src-tauri/src/agentic.rs b/src-tauri/src/agentic.rs index f6b9f23..d54c5e2 100644 --- a/src-tauri/src/agentic.rs +++ b/src-tauri/src/agentic.rs @@ -5,8 +5,8 @@ use anyhow::{Result, anyhow}; use crate::file_operations::{read_directory_contents, search_in_files, read_file_contents, write_file_contents, open_with_default_app}; use crate::system_operations::{ get_installed_applications, launch_application, execute_terminal_command, - perform_file_operation, get_running_processes, kill_process, check_permission_level, - FileSystemOperation, FileOperationType, PermissionLevel}; + perform_file_operation, get_running_processes, kill_process, + FileSystemOperation, FileOperationType}; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct AgentAction { pub action_type: String, @@ -226,6 +226,89 @@ impl AgentSession { }, ], }, + AgentCapability { + name: "launch_application".to_string(), + description: "Launch an installed application".to_string(), + parameters: vec![ + AgentParameter { + name: "app_name".to_string(), + parameter_type: "string".to_string(), + description: "Name or path of the application to launch".to_string(), + required: true, + default_value: None, + }, + ], + }, + AgentCapability { + name: "get_installed_apps".to_string(), + description: "Get a list of installed applications on the system".to_string(), + parameters: vec![], + }, + AgentCapability { + name: "execute_command".to_string(), + description: "Execute a terminal/shell command".to_string(), + parameters: vec![ + AgentParameter { + name: "command".to_string(), + parameter_type: "string".to_string(), + description: "The command to execute".to_string(), + required: true, + default_value: None, + }, + AgentParameter { + name: "working_directory".to_string(), + parameter_type: "string".to_string(), + description: "Working directory for command execution".to_string(), + required: false, + default_value: None, + }, + ], + }, + AgentCapability { + name: "file_operation".to_string(), + description: "Perform file system operations (copy, move, delete files/directories)".to_string(), + parameters: vec![ + AgentParameter { + name: "operation".to_string(), + parameter_type: "string".to_string(), + description: "Type of operation: 'copy', 'move', 'delete', 'rename'".to_string(), + required: true, + default_value: None, + }, + AgentParameter { + name: "source".to_string(), + parameter_type: "string".to_string(), + description: "Source file or directory path".to_string(), + required: true, + default_value: None, + }, + AgentParameter { + name: "destination".to_string(), + parameter_type: "string".to_string(), + description: "Destination path (for copy, move, rename operations)".to_string(), + required: false, + default_value: None, + }, + ], + }, + AgentCapability { + name: "get_processes".to_string(), + description: "Get a list of running processes on the system".to_string(), + parameters: vec![], + }, + AgentCapability { + name: "kill_process".to_string(), + description: "Terminate a running process by PID".to_string(), + parameters: vec![ + AgentParameter { + name: "pid".to_string(), + parameter_type: "number".to_string(), + description: "Process ID (PID) to terminate".to_string(), + required: true, + default_value: None, + }, + ], + }, ] } @@ -378,9 +461,9 @@ impl AgentSession { } async fn execute_launch_application(&self, params: &HashMap) -> Result { - let app_path = params.get("path") + let app_path = params.get("app_name") .and_then(|v| v.as_str()) - .ok_or_else(|| anyhow!("Missing required parameter: path"))?; + .ok_or_else(|| anyhow!("Missing required parameter: app_name"))?; let args = params.get("arguments") .and_then(|v| v.as_array()) @@ -413,13 +496,8 @@ impl AgentSession { self.current_directory.lock().ok() .map(|dir| dir.clone()) }); - - // Check permission level - let permission = check_permission_level("execute_command", params); - if permission.level == PermissionLevel::Dangerous { - return Err(anyhow!("Command requires explicit user permission: {}", command)); - } - + + // Permission is checked at frontend level before this is called let result = execute_terminal_command(command, working_dir.as_deref())?; Ok(serde_json::to_value(result)?) } @@ -471,13 +549,8 @@ impl AgentSession { .and_then(|v| v.as_u64()) .map(|v| v as u32) .ok_or_else(|| anyhow!("Missing required parameter: pid"))?; - - // Check permission level - let permission = check_permission_level("kill_process", params); - if permission.level == PermissionLevel::Dangerous { - return Err(anyhow!("Killing process requires explicit user permission: PID {}", pid)); - } - + + // Permission is checked at frontend level before this is called kill_process(pid)?; Ok(serde_json::Value::String(format!("Successfully terminated process with PID: {}", pid))) } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 834c707..cc34c26 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1,19 +1,24 @@ use crate::database::Database; use crate::models::*; use crate::file_operations::{ - open_with_default_app, read_directory_contents, search_in_files, + open_with_default_app, read_directory_contents, search_in_files, read_file_contents, write_file_contents, DirectoryContents, SearchResult }; use crate::agentic::{AgentSession, AgentAction, AgentCapability}; +use crate::tools::get_all_tool_definitions; use crate::system_operations::{ launch_application, get_installed_applications, execute_terminal_command, perform_file_operation, get_running_processes, kill_process, check_permission_level, FileSystemOperation, FileOperationType, PermissionLevel, AppInfo, CommandResult, ProcessInfo }; +use crate::llm_streaming; +use crate::permission_manager::PermissionManager; +use crate::mcp_client::McpClientManager; +use crate::tool_registry::ToolRegistry; use tauri::{State, Emitter}; use serde_json::json; use std::collections::HashMap; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; #[tauri::command] pub async fn create_chat(db: State<'_, Database>, request: CreateChatRequest) -> Result { @@ -53,7 +58,7 @@ pub async fn create_message( db: State<'_, Database>, request: CreateMessageRequest, ) -> Result { - db.create_message(request.chat_id, request.content, request.role, request.images) + db.create_message(request.chat_id, request.content, request.role, request.images, request.permission_request_id) .await .map_err(|e| e.to_string()) } @@ -114,201 +119,301 @@ pub async fn delete_api_config(db: State<'_, Database>, config_id: String) -> Re .map_err(|e| e.to_string()) } +// Tool Execution Commands #[tauri::command] -pub async fn send_ai_message( +pub async fn create_tool_execution( db: State<'_, Database>, - chat_id: String, - user_message: String, -) -> Result { - // Get the chat to find its API config - let chat = db.get_chat(&chat_id).await.map_err(|e| e.to_string())?; - let chat = chat.ok_or("Chat not found")?; + request: CreateToolExecutionRequest, +) -> Result { + db.create_tool_execution(request) + .await + .map_err(|e| e.to_string()) +} - // Get API config (use chat's config or default) - let api_config = if let Some(config_id) = &chat.api_config_id { - db.get_api_config(config_id).await.map_err(|e| e.to_string())? - } else { - db.get_default_api_config().await.map_err(|e| e.to_string())? - }; +#[tauri::command] +pub async fn get_tool_executions_for_message( + db: State<'_, Database>, + message_id: String, +) -> Result, String> { + db.get_tool_executions_for_message(&message_id) + .await + .map_err(|e| e.to_string()) +} - let api_config = api_config.ok_or("No API configuration found")?; +#[tauri::command] +pub async fn get_tool_executions_for_messages( + db: State<'_, Database>, + message_ids: Vec, +) -> Result>, String> { + db.get_tool_executions_for_messages(&message_ids) + .await + .map_err(|e| e.to_string()) +} - // Create user message - let _user_msg = db.create_message(chat_id.clone(), user_message.clone(), MessageRole::User, None) +// MCP Server Commands +#[tauri::command] +pub async fn create_mcp_server( + db: State<'_, Database>, + request: CreateMcpServerRequest, +) -> Result { + db.create_mcp_server(request) .await - .map_err(|e| e.to_string())?; + .map_err(|e| e.to_string()) +} - // Get recent messages for context - let messages = db.get_messages(&chat_id).await.map_err(|e| e.to_string())?; - - // Convert to chat format (take last 10 messages for context) - let chat_messages: Vec = messages - .iter() - .rev() - .take(10) - .rev() - .map(|msg| { - let content = if let Some(images) = &msg.images { - if !images.is_empty() { - // Create vision format with text and images - let mut content_array = vec![]; - - // Add text content if present - if !msg.content.is_empty() { - content_array.push(json!({ - "type": "text", - "text": msg.content - })); - } - - // Add images - for image in images { - content_array.push(json!({ - "type": "image_url", - "image_url": { - "url": image - } - })); - } - - json!(content_array) - } else { - // No images, just text - json!(msg.content) - } - } else { - // No images, just text - json!(msg.content) - }; +#[tauri::command] +pub async fn get_mcp_servers(db: State<'_, Database>) -> Result, String> { + db.get_mcp_servers().await.map_err(|e| e.to_string()) +} - ChatMessage { - role: match msg.role { - MessageRole::User => "user".to_string(), - MessageRole::Assistant => "assistant".to_string(), - }, - content, - } - }) - .collect(); +#[tauri::command] +pub async fn get_mcp_server(db: State<'_, Database>, server_id: String) -> Result, String> { + db.get_mcp_server(&server_id).await.map_err(|e| e.to_string()) +} - // Send to LLM - let ai_response = db.send_chat_completion(&api_config, chat_messages) +#[tauri::command] +pub async fn update_mcp_server( + db: State<'_, Database>, + server_id: String, + request: UpdateMcpServerRequest, +) -> Result { + db.update_mcp_server(&server_id, request) .await - .map_err(|e| e.to_string())?; + .map_err(|e| e.to_string()) +} - // Create assistant message - let assistant_msg = db.create_message(chat_id, ai_response, MessageRole::Assistant, None) +#[tauri::command] +pub async fn delete_mcp_server(db: State<'_, Database>, server_id: String) -> Result<(), String> { + db.delete_mcp_server(&server_id) .await - .map_err(|e| e.to_string())?; + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn get_mcp_tools_for_server(db: State<'_, Database>, server_id: String) -> Result, String> { + db.get_mcp_tools_for_server(&server_id) + .await + .map_err(|e| e.to_string()) +} - Ok(assistant_msg) +#[tauri::command] +pub async fn get_enabled_mcp_tools(db: State<'_, Database>) -> Result, String> { + db.get_enabled_mcp_tools().await.map_err(|e| e.to_string()) } #[tauri::command] -pub async fn send_ai_message_streaming( - window: tauri::Window, +pub async fn toggle_mcp_tool(db: State<'_, Database>, tool_id: String, enabled: bool) -> Result<(), String> { + db.toggle_mcp_tool(&tool_id, enabled) + .await + .map_err(|e| e.to_string()) +} + +// MCP Connection Commands +#[tauri::command] +pub async fn connect_mcp_server( db: State<'_, Database>, - chat_id: String, - user_message: String, - images: Option>, -) -> Result { - // Get the chat to find its API config - let chat = db.get_chat(&chat_id).await.map_err(|e| e.to_string())?; - let chat = chat.ok_or("Chat not found")?; + mcp_manager: State<'_, Arc>, + tool_registry: State<'_, Arc>, + server_id: String, +) -> Result { + // Get server config from database + let server = db.get_mcp_server(&server_id) + .await + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("Server not found: {}", server_id))?; - // Get API config (use chat's config or default) - let api_config = if let Some(config_id) = &chat.api_config_id { - db.get_api_config(config_id).await.map_err(|e| e.to_string())? - } else { - db.get_default_api_config().await.map_err(|e| e.to_string())? - }; + // Add to manager and connect + let client = mcp_manager.add_server(server).await; + client.connect().await.map_err(|e| e.to_string())?; - let api_config = api_config.ok_or("No API configuration found")?; + // Get tools and sync to database + let tools = client.get_tools().await; + let tool_data: Vec<(String, Option, serde_json::Value)> = tools + .iter() + .map(|t| (t.name.clone(), t.description.clone(), t.input_schema.clone())) + .collect(); - // Create user message - let user_msg = db.create_message(chat_id.clone(), user_message.clone(), MessageRole::User, images) + db.sync_mcp_tools(&server_id, tool_data) .await .map_err(|e| e.to_string())?; - // Emit user message to frontend - window.emit("message_created", &user_msg).map_err(|e| e.to_string())?; - - // Get recent messages for context - let messages = db.get_messages(&chat_id).await.map_err(|e| e.to_string())?; - - // Convert to chat format (take last 10 messages for context) - let chat_messages: Vec = messages - .iter() - .rev() - .take(10) - .rev() - .map(|msg| { - let content = if let Some(images) = &msg.images { - if !images.is_empty() { - // Create vision format with text and images - let mut content_array = vec![]; - - // Add text content if present - if !msg.content.is_empty() { - content_array.push(json!({ - "type": "text", - "text": msg.content - })); - } - - // Add images - for image in images { - content_array.push(json!({ - "type": "image_url", - "image_url": { - "url": image - } - })); - } - - json!(content_array) - } else { - // No images, just text - json!(msg.content) - } - } else { - // No images, just text - json!(msg.content) - }; + // Refresh tool registry cache + tool_registry.refresh_mcp_tools() + .await + .map_err(|e| e.to_string())?; - ChatMessage { - role: match msg.role { - MessageRole::User => "user".to_string(), - MessageRole::Assistant => "assistant".to_string(), - }, - content, - } - }) - .collect(); + Ok(json!({ + "status": "connected", + "tools_count": tools.len() + })) +} - // Create a placeholder assistant message for streaming - let assistant_msg_id = uuid::Uuid::new_v4().to_string(); - - // Emit streaming start event - window.emit("streaming_start", json!({ - "message_id": assistant_msg_id, - "chat_id": chat_id - })).map_err(|e| e.to_string())?; - - // Send to LLM with streaming - let ai_response = db.send_chat_completion_streaming(&api_config, chat_messages, &window, &assistant_msg_id, &chat_id) +#[tauri::command] +pub async fn disconnect_mcp_server( + mcp_manager: State<'_, Arc>, + tool_registry: State<'_, Arc>, + server_id: String, +) -> Result<(), String> { + mcp_manager.disconnect_server(&server_id) .await .map_err(|e| e.to_string())?; - // Create final assistant message in database - let assistant_msg = db.create_message(chat_id, ai_response, MessageRole::Assistant, None) + // Refresh tool registry cache + tool_registry.refresh_mcp_tools() .await .map_err(|e| e.to_string())?; - // Emit final message created event - window.emit("final_message_created", &assistant_msg).map_err(|e| e.to_string())?; + Ok(()) +} + +#[tauri::command] +pub async fn get_mcp_server_status( + mcp_manager: State<'_, Arc>, + server_id: String, +) -> Result { + let status = mcp_manager.get_server_status(&server_id) + .await + .unwrap_or(crate::mcp_client::ConnectionStatus::Disconnected); + + let status_str = match status { + crate::mcp_client::ConnectionStatus::Disconnected => "disconnected", + crate::mcp_client::ConnectionStatus::Connecting => "connecting", + crate::mcp_client::ConnectionStatus::Connected => "connected", + crate::mcp_client::ConnectionStatus::Error(ref e) => return Ok(format!("error: {}", e)), + }; + + Ok(status_str.to_string()) +} + +#[tauri::command] +pub async fn get_all_mcp_statuses( + mcp_manager: State<'_, Arc>, +) -> Result, String> { + let statuses = mcp_manager.get_all_statuses().await; + + let result: HashMap = statuses + .into_iter() + .map(|(id, status)| { + let status_str = match status { + crate::mcp_client::ConnectionStatus::Disconnected => "disconnected".to_string(), + crate::mcp_client::ConnectionStatus::Connecting => "connecting".to_string(), + crate::mcp_client::ConnectionStatus::Connected => "connected".to_string(), + crate::mcp_client::ConnectionStatus::Error(e) => format!("error: {}", e), + }; + (id, status_str) + }) + .collect(); + + Ok(result) +} + +#[tauri::command] +pub async fn get_merged_tool_definitions( + tool_registry: State<'_, Arc>, +) -> Result, String> { + Ok(tool_registry.get_all_tool_definitions().await) +} + +/// Execute a tool, routing to builtin or MCP based on tool source +#[tauri::command] +pub async fn execute_tool_routed( + agent_sessions: State<'_, Mutex>>, + mcp_manager: State<'_, Arc>, + tool_registry: State<'_, Arc>, + session_id: String, + tool_name: String, + parameters: serde_json::Value, +) -> Result { + // Find tool source + let source = tool_registry.find_tool_source(&tool_name).await + .ok_or_else(|| format!("Unknown tool: {}", tool_name))?; + + match source { + crate::tool_registry::ToolSource::Builtin => { + // Use existing builtin execution path + let params: HashMap = match parameters { + serde_json::Value::Object(map) => map.into_iter().collect(), + _ => HashMap::new(), + }; + + // Clone the session to avoid holding the lock across await + let session = { + let sessions = agent_sessions.lock().map_err(|e| e.to_string())?; + sessions.get(&session_id) + .ok_or_else(|| format!("Session {} not found", session_id))? + .clone() + }; + + session.execute_action(&tool_name, params) + .await + .map_err(|e| e.to_string()) + } + crate::tool_registry::ToolSource::McpServer(server_id) => { + // Get original tool name from prefixed name + let original_name = tool_registry.get_mcp_tool_info(&tool_name).await + .map(|(_, name)| name) + .ok_or_else(|| format!("Could not find MCP tool info for: {}", tool_name))?; - Ok(assistant_msg.id) + // Execute via MCP + let result = mcp_manager.call_tool(&server_id, &original_name, Some(parameters.clone())) + .await + .map_err(|e| e.to_string())?; + + // Convert MCP result to AgentAction + let (success, result_value, error_message) = if result.is_error { + let error_text: String = result.content.iter() + .filter_map(|c| match c { + crate::mcp_client::ToolContent::Text { text } => Some(text.clone()), + _ => None, + }) + .collect::>() + .join("\n"); + (false, None, Some(error_text)) + } else { + // Convert content to JSON + let contents: Vec = result.content.iter() + .map(|c| match c { + crate::mcp_client::ToolContent::Text { text } => { + serde_json::json!({ "type": "text", "text": text }) + } + crate::mcp_client::ToolContent::Image { data, mime_type } => { + serde_json::json!({ "type": "image", "data": data, "mimeType": mime_type }) + } + crate::mcp_client::ToolContent::Resource { resource } => { + serde_json::json!({ + "type": "resource", + "uri": resource.uri, + "mimeType": resource.mime_type, + "text": resource.text + }) + } + }) + .collect(); + + let result_value = if contents.len() == 1 { + contents.into_iter().next() + } else { + Some(serde_json::Value::Array(contents)) + }; + + (true, result_value, None) + }; + + let params_map: HashMap = match parameters { + serde_json::Value::Object(map) => map.into_iter().collect(), + _ => HashMap::new(), + }; + + Ok(AgentAction { + action_type: tool_name, + description: format!("MCP tool execution via {}", server_id), + parameters: params_map, + result: result_value, + success, + error_message, + }) + } + } } // File Operations Commands @@ -378,6 +483,11 @@ pub async fn get_agent_capabilities() -> Result, String> { Ok(AgentSession::get_capabilities()) } +#[tauri::command] +pub async fn get_agent_tool_definitions() -> Result, String> { + Ok(get_all_tool_definitions()) +} + #[tauri::command] pub async fn execute_agent_action( agent_sessions: State<'_, Mutex>>, @@ -432,26 +542,176 @@ pub async fn create_or_get_agent_session( #[tauri::command] pub async fn request_permission( window: tauri::Window, + db: State<'_, Database>, + permission_manager: State<'_, PermissionManager>, operation: String, parameters: HashMap, + chat_id: Option, ) -> Result { - let permission = check_permission_level(&operation, ¶meters); - - // Emit permission request to frontend - window.emit("permission_request", json!({ - "operation": permission.operation, - "description": permission.description, - "level": permission.level, - "details": permission.details, - })).map_err(|e| e.to_string())?; - - // In a real implementation, you would wait for user response - // For now, we'll return based on permission level - match permission.level { - PermissionLevel::Safe => Ok(true), - PermissionLevel::Moderate => Ok(true), // Should wait for user confirmation - PermissionLevel::Dangerous => Ok(false), // Should require explicit permission + let perm_check = check_permission_level(&operation, ¶meters); + + // Convert to our permission request type + let mut details = HashMap::new(); + for (k, v) in perm_check.details { + details.insert(k, v); + } + + let level_str = match perm_check.level { + PermissionLevel::Safe => "Safe", + PermissionLevel::Moderate => "Moderate", + PermissionLevel::Dangerous => "Dangerous", + }; + + let level_converted = match perm_check.level { + PermissionLevel::Safe => crate::permission_manager::PermissionLevel::Safe, + PermissionLevel::Moderate => crate::permission_manager::PermissionLevel::Moderate, + PermissionLevel::Dangerous => crate::permission_manager::PermissionLevel::Dangerous, + }; + + println!("[RUST] request_permission called for operation: {}, chat_id: {:?}, level: {:?}", + operation, chat_id, level_str); + + // Check if this should be auto-approved (Safe, cached, or already pending) + let is_cached = permission_manager.is_permission_cached( + chat_id.clone(), + operation.clone() + ).await; + + println!("[RUST] Cache check result for operation '{}' in chat {:?}: is_cached = {}", + operation, chat_id, is_cached); + + let is_pending = permission_manager.has_pending_permission( + chat_id.clone(), + &operation + ).await; + + println!("[RUST] Pending check result for operation '{}' in chat {:?}: is_pending = {}", + operation, chat_id, is_pending); + + // If Safe or cached, auto-approve + if level_converted == crate::permission_manager::PermissionLevel::Safe || is_cached { + println!("[RUST] ✅ Auto-approving: Safe={}, Cached={}", + level_converted == crate::permission_manager::PermissionLevel::Safe, is_cached); + return Ok(true); } + + // If already pending, wait a moment and return error to avoid duplicate + if is_pending { + println!("[RUST] Another permission request for '{}' is already pending, skipping duplicate", operation); + return Err("Permission request already pending. Please respond to the existing request.".to_string()); + } + + // Create permission record in database + let chat_id_str = chat_id.clone().ok_or_else(|| "chat_id required for permission request".to_string())?; + + let permission = db.create_permission_request( + chat_id_str.clone(), + operation.clone(), + perm_check.description.clone(), + level_str.to_string(), + details.clone(), + "pending".to_string(), + ).await.map_err(|e| e.to_string())?; + + println!("[RUST] Created permission record with ID: {}", permission.id); + + // Create system message linked to this permission + let mut message = db.create_message( + chat_id_str.clone(), + format!("Permission required for operation: {}", operation), + MessageRole::System, + None, + Some(permission.id.clone()), + ).await.map_err(|e| e.to_string())?; + + println!("[RUST] Created system message with ID: {}", message.id); + + // Populate the permission_request field for the message + message.permission_request = Some(permission.clone()); + + // Emit event to notify frontend of new permission message + window.emit("permission_message_created", &message) + .map_err(|e| e.to_string())?; + + println!("[RUST] Emitted permission_message_created event"); + + // Create permission manager request for oneshot channel communication + // IMPORTANT: Use the same ID as the database permission so frontend can respond + // IMPORTANT: Use the original operation name (snake_case) for cache consistency + let pm_request = crate::permission_manager::PermissionRequest { + id: permission.id.clone(), // Use the database permission ID + operation: operation.clone(), // Use snake_case tool name for cache consistency + description: perm_check.description.clone(), + level: level_converted, + details, + chat_id, + }; + + // Wait for user response via permission manager + let result = permission_manager.request_permission(pm_request).await; + + println!("[RUST] Permission response received: {:?}", result); + + // Update database with result + match result { + Ok(approved) => { + let status = if approved { "approved" } else { "denied" }; + db.update_permission_status(&permission.id, status.to_string()) + .await + .map_err(|e| e.to_string())?; + println!("[RUST] Updated permission status to: {}", status); + + // Return the approval status - frontend will handle stopping execution if denied + Ok(approved) + } + Err(e) => Err(e), + } +} + +#[tauri::command] +pub async fn respond_to_permission( + db: State<'_, Database>, + permission_manager: State<'_, PermissionManager>, + request_id: String, + approved: bool, +) -> Result<(), String> { + println!("[RUST] respond_to_permission called for ID: {}, approved: {}", request_id, approved); + + // Update database + let status = if approved { "approved" } else { "denied" }; + db.update_permission_status(&request_id, status.to_string()) + .await + .map_err(|e| e.to_string())?; + + // Notify permission manager (oneshot channel) + let result = permission_manager.respond_to_permission(request_id.clone(), approved).await; + println!("[RUST] respond_to_permission result for ID {}: {:?}", request_id, result); + result +} + +#[tauri::command] +pub async fn clear_chat_permissions( + permission_manager: State<'_, PermissionManager>, + chat_id: String, +) -> Result<(), String> { + permission_manager.clear_chat_permissions(chat_id).await +} + +#[tauri::command] +pub async fn clear_permission( + permission_manager: State<'_, PermissionManager>, + chat_id: String, + operation: String, +) -> Result<(), String> { + permission_manager.clear_permission(chat_id, operation).await +} + +#[tauri::command] +pub async fn get_chat_permissions( + permission_manager: State<'_, PermissionManager>, + chat_id: String, +) -> Result, String> { + Ok(permission_manager.get_chat_permissions(chat_id).await) } #[tauri::command] @@ -610,6 +870,348 @@ pub async fn terminate_process( kill_process(pid) .map_err(|e| e.to_string())?; - + Ok(format!("Successfully terminated process with PID: {}", pid)) } + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct ModelInfo { + pub id: String, + pub name: String, + pub description: Option, +} + +/// Fetch available models from provider API (bypasses CORS) +#[tauri::command] +pub async fn fetch_provider_models( + provider: String, + api_key: String, + base_url: Option, +) -> Result, String> { + let client = reqwest::Client::new(); + + match provider.as_str() { + "openai" => { + let url = base_url.unwrap_or_else(|| "https://api.openai.com/v1".to_string()); + fetch_openai_models(&client, &api_key, &url).await + } + "anthropic" => { + Ok(get_anthropic_models()) + } + "deepseek" => { + let url = base_url.unwrap_or_else(|| "https://api.deepseek.com/v1".to_string()); + fetch_openai_compatible_models(&client, &api_key, &url).await + } + "lmstudio" => { + let url = base_url.unwrap_or_else(|| "http://localhost:1234/v1".to_string()); + fetch_openai_compatible_models(&client, &api_key, &url).await + } + "mistral" => { + let url = base_url.unwrap_or_else(|| "https://api.mistral.ai/v1".to_string()); + fetch_openai_compatible_models(&client, &api_key, &url).await + } + "kimi" => { + let url = base_url.unwrap_or_else(|| "https://api.moonshot.cn/v1".to_string()); + fetch_openai_compatible_models(&client, &api_key, &url).await + } + "openrouter" => { + let url = base_url.unwrap_or_else(|| "https://openrouter.ai/api/v1".to_string()); + fetch_openai_compatible_models(&client, &api_key, &url).await + } + "together" => { + let url = base_url.unwrap_or_else(|| "https://api.together.xyz/v1".to_string()); + fetch_openai_compatible_models(&client, &api_key, &url).await + } + "groq" => { + let url = base_url.unwrap_or_else(|| "https://api.groq.com/openai/v1".to_string()); + fetch_openai_compatible_models(&client, &api_key, &url).await + } + "perplexity" => { + let url = base_url.unwrap_or_else(|| "https://api.perplexity.ai".to_string()); + fetch_openai_compatible_models(&client, &api_key, &url).await + } + "ollama" => { + let mut url = base_url.unwrap_or_else(|| "http://localhost:11434".to_string()); + // Strip /v1 suffix for Ollama + if url.ends_with("/v1") || url.ends_with("/v1/") { + url = url.trim_end_matches('/').trim_end_matches("/v1").to_string(); + } + fetch_ollama_models(&client, &url).await + } + "google" => { + Ok(get_google_models()) + } + "custom" => { + if let Some(url) = base_url { + fetch_openai_compatible_models(&client, &api_key, &url).await + } else { + Ok(vec![]) + } + } + _ => Ok(vec![]), + } +} + +async fn fetch_openai_models( + client: &reqwest::Client, + api_key: &str, + base_url: &str, +) -> Result, String> { + let url = format!("{}/models", base_url); + + let response = client + .get(&url) + .header("Authorization", format!("Bearer {}", api_key)) + .send() + .await + .map_err(|e| format!("Failed to fetch models: {}", e))?; + + if !response.status().is_success() { + return Err(format!("API returned error: {}", response.status())); + } + + let json: serde_json::Value = response + .json() + .await + .map_err(|e| format!("Failed to parse response: {}", e))?; + + let models = json["data"] + .as_array() + .ok_or("Invalid response format")? + .iter() + .filter_map(|m| { + let id = m["id"].as_str()?; + if id.contains("gpt") { + Some(ModelInfo { + id: id.to_string(), + name: id.to_string(), + description: None, + }) + } else { + None + } + }) + .collect(); + + Ok(models) +} + +async fn fetch_openai_compatible_models( + client: &reqwest::Client, + api_key: &str, + base_url: &str, +) -> Result, String> { + let url = format!("{}/models", base_url); + + let response = client + .get(&url) + .header("Authorization", format!("Bearer {}", api_key)) + .send() + .await + .map_err(|e| format!("Failed to fetch models: {}", e))?; + + if !response.status().is_success() { + return Err(format!("API returned error: {}", response.status())); + } + + let json: serde_json::Value = response + .json() + .await + .map_err(|e| format!("Failed to parse response: {}", e))?; + + let models = json["data"] + .as_array() + .ok_or("Invalid response format")? + .iter() + .filter_map(|m| { + let id = m["id"].as_str()?; + Some(ModelInfo { + id: id.to_string(), + name: id.to_string(), + description: None, + }) + }) + .collect(); + + Ok(models) +} + +async fn fetch_ollama_models( + client: &reqwest::Client, + base_url: &str, +) -> Result, String> { + let url = format!("{}/api/tags", base_url); + + let response = client + .get(&url) + .send() + .await + .map_err(|e| format!("Failed to fetch Ollama models: {}", e))?; + + if !response.status().is_success() { + return Err(format!("Ollama API returned error: {}", response.status())); + } + + let json: serde_json::Value = response + .json() + .await + .map_err(|e| format!("Failed to parse Ollama response: {}", e))?; + + let models = json["models"] + .as_array() + .ok_or("Invalid Ollama response format")? + .iter() + .filter_map(|m| { + let name = m["name"].as_str()?; + let size = m["size"].as_u64().unwrap_or(0); + Some(ModelInfo { + id: name.to_string(), + name: name.to_string(), + description: Some(format!("Size: {}", format_bytes(size))), + }) + }) + .collect(); + + Ok(models) +} + +fn get_anthropic_models() -> Vec { + vec![ + ModelInfo { + id: "claude-3-5-sonnet-20241022".to_string(), + name: "Claude 3.5 Sonnet".to_string(), + description: Some("Most intelligent model".to_string()), + }, + ModelInfo { + id: "claude-3-5-haiku-20241022".to_string(), + name: "Claude 3.5 Haiku".to_string(), + description: Some("Fastest model".to_string()), + }, + ModelInfo { + id: "claude-3-opus-20240229".to_string(), + name: "Claude 3 Opus".to_string(), + description: Some("Powerful model for complex tasks".to_string()), + }, + ModelInfo { + id: "claude-3-sonnet-20240229".to_string(), + name: "Claude 3 Sonnet".to_string(), + description: Some("Balanced model".to_string()), + }, + ModelInfo { + id: "claude-3-haiku-20240307".to_string(), + name: "Claude 3 Haiku".to_string(), + description: Some("Fast and efficient".to_string()), + }, + ] +} + +fn get_google_models() -> Vec { + vec![ + ModelInfo { + id: "gemini-2.0-flash-exp".to_string(), + name: "Gemini 2.0 Flash (Experimental)".to_string(), + description: Some("Latest experimental model".to_string()), + }, + ModelInfo { + id: "gemini-1.5-pro".to_string(), + name: "Gemini 1.5 Pro".to_string(), + description: Some("Most capable model".to_string()), + }, + ModelInfo { + id: "gemini-1.5-flash".to_string(), + name: "Gemini 1.5 Flash".to_string(), + description: Some("Fast and efficient".to_string()), + }, + ModelInfo { + id: "gemini-1.0-pro".to_string(), + name: "Gemini 1.0 Pro".to_string(), + description: Some("Stable production model".to_string()), + }, + ] +} + +fn format_bytes(bytes: u64) -> String { + if bytes == 0 { + return "0 Bytes".to_string(); + } + + let k: f64 = 1024.0; + let sizes = ["Bytes", "KB", "MB", "GB"]; + let i = (bytes as f64).log(k).floor() as usize; + let size = (bytes as f64) / k.powi(i as i32); + + format!("{:.2} {}", size, sizes[i.min(3)]) +} + +/// Stream LLM request through Rust backend (bypasses CORS and Tauri HTTP plugin issues) +#[tauri::command] +pub async fn stream_llm_request( + window: tauri::Window, + provider: String, + api_key: String, + base_url: Option, + model: String, + messages: Vec, + tools: Option>, + temperature: f32, + max_tokens: Option, + stream_id: String, +) -> Result<(), String> { + // Route to appropriate streaming function based on provider + match provider.as_str() { + "anthropic" => { + llm_streaming::stream_anthropic( + &window, + &api_key, + &model, + messages, + tools, + temperature, + max_tokens, + &stream_id, + ) + .await + } + "google" => { + llm_streaming::stream_google( + &window, + &api_key, + &model, + messages, + tools, + temperature, + &stream_id, + ) + .await + } + "openai" | "deepseek" | "mistral" | "lmstudio" | "kimi" + | "openrouter" | "together" | "groq" | "perplexity" | "ollama" | "custom" => { + let base_url = base_url.unwrap_or_else(|| { + llm_streaming::get_default_base_url(&provider) + }); + llm_streaming::stream_openai_compatible( + &window, + &base_url, + &api_key, + &model, + messages, + tools, + temperature, + max_tokens, + &stream_id, + ) + .await + } + _ => Err(format!("Provider '{}' is not yet supported for streaming", provider)), + } +} + +#[tauri::command] +pub fn get_system_info() -> Result { + Ok(json!({ + "os": std::env::consts::OS, + "arch": std::env::consts::ARCH, + "family": std::env::consts::FAMILY, + })) +} + diff --git a/src-tauri/src/database.rs b/src-tauri/src/database.rs index 5d68b75..84a2739 100644 --- a/src-tauri/src/database.rs +++ b/src-tauri/src/database.rs @@ -3,9 +3,6 @@ use chrono::Utc; use sqlx::{migrate::MigrateDatabase, Pool, Sqlite, SqlitePool, Row}; use std::path::PathBuf; use uuid::Uuid; -use reqwest::Client; -use serde_json::json; -use tauri::Emitter; use crate::models::*; @@ -145,10 +142,10 @@ impl Database { } // Message operations - pub async fn create_message(&self, chat_id: String, content: String, role: MessageRole, images: Option>) -> Result { + pub async fn create_message(&self, chat_id: String, content: String, role: MessageRole, images: Option>, permission_request_id: Option) -> Result { let id = Uuid::new_v4().to_string(); let now = Utc::now(); - + // Serialize images to JSON string if present let images_json = match images.as_ref() { Some(imgs) if !imgs.is_empty() => Some(serde_json::to_string(imgs)?), @@ -156,7 +153,7 @@ impl Database { }; sqlx::query( - "INSERT INTO messages (id, chat_id, content, role, created_at, images) VALUES (?, ?, ?, ?, ?, ?)" + "INSERT INTO messages (id, chat_id, content, role, created_at, images, permission_request_id) VALUES (?, ?, ?, ?, ?, ?, ?)" ) .bind(&id) .bind(&chat_id) @@ -164,6 +161,7 @@ impl Database { .bind(&role) .bind(now) .bind(&images_json) + .bind(&permission_request_id) .execute(&self.pool) .await?; @@ -181,15 +179,26 @@ impl Database { role, created_at: now, images, + permission_request_id, + permission_request: None, }) } pub async fn get_messages(&self, chat_id: &str) -> Result> { - // Use the full query with images column - let rows = sqlx::query("SELECT id, chat_id, content, role, created_at, images FROM messages WHERE chat_id = ? ORDER BY created_at ASC") - .bind(chat_id) - .fetch_all(&self.pool) - .await?; + // Query messages with optional permission_request join + let rows = sqlx::query( + "SELECT + m.id, m.chat_id, m.content, m.role, m.created_at, m.images, m.permission_request_id, + p.id as perm_id, p.chat_id as perm_chat_id, p.operation, p.description, p.level, + p.details, p.status, p.created_at as perm_created_at, p.updated_at as perm_updated_at + FROM messages m + LEFT JOIN permission_requests p ON m.permission_request_id = p.id + WHERE m.chat_id = ? + ORDER BY m.created_at ASC" + ) + .bind(chat_id) + .fetch_all(&self.pool) + .await?; let mut messages = Vec::new(); for row in rows { @@ -197,6 +206,7 @@ impl Database { let role = match role_str.as_str() { "user" => MessageRole::User, "assistant" => MessageRole::Assistant, + "system" => MessageRole::System, _ => return Err(anyhow::anyhow!("Invalid message role: {}", role_str)), }; @@ -206,6 +216,31 @@ impl Database { None => None, }; + let permission_request_id: Option = row.try_get("permission_request_id")?; + + // Build permission_request if joined + let permission_request = if let Some(perm_id) = row.try_get::, _>("perm_id")? { + let details_json: Option = row.try_get("details")?; + let details = match details_json { + Some(json_str) => serde_json::from_str(&json_str).unwrap_or_default(), + None => std::collections::HashMap::new(), + }; + + Some(crate::models::PermissionRequest { + id: perm_id, + chat_id: row.try_get("perm_chat_id")?, + operation: row.try_get("operation")?, + description: row.try_get("description")?, + level: row.try_get("level")?, + details, + status: row.try_get("status")?, + created_at: row.try_get("perm_created_at")?, + updated_at: row.try_get("perm_updated_at")?, + }) + } else { + None + }; + messages.push(Message { id: row.try_get("id")?, chat_id: row.try_get("chat_id")?, @@ -213,6 +248,8 @@ impl Database { role, created_at: row.try_get("created_at")?, images, + permission_request_id, + permission_request, }); } @@ -228,6 +265,123 @@ impl Database { Ok(()) } + // Permission operations + pub async fn create_permission_request( + &self, + chat_id: String, + operation: String, + description: String, + level: String, + details: std::collections::HashMap, + status: String, + ) -> Result { + let id = Uuid::new_v4().to_string(); + let now = Utc::now(); + + // Serialize details to JSON string + let details_json = serde_json::to_string(&details)?; + + sqlx::query( + "INSERT INTO permission_requests (id, chat_id, operation, description, level, details, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" + ) + .bind(&id) + .bind(&chat_id) + .bind(&operation) + .bind(&description) + .bind(&level) + .bind(&details_json) + .bind(&status) + .bind(now) + .bind(now) + .execute(&self.pool) + .await?; + + Ok(crate::models::PermissionRequest { + id, + chat_id, + operation, + description, + level, + details, + status, + created_at: now, + updated_at: now, + }) + } + + pub async fn get_permission_request(&self, permission_id: &str) -> Result> { + let row = sqlx::query( + "SELECT id, chat_id, operation, description, level, details, status, created_at, updated_at FROM permission_requests WHERE id = ?" + ) + .bind(permission_id) + .fetch_optional(&self.pool) + .await?; + + match row { + Some(row) => { + let details_json: String = row.try_get("details")?; + let details = serde_json::from_str(&details_json).unwrap_or_default(); + + Ok(Some(crate::models::PermissionRequest { + id: row.try_get("id")?, + chat_id: row.try_get("chat_id")?, + operation: row.try_get("operation")?, + description: row.try_get("description")?, + level: row.try_get("level")?, + details, + status: row.try_get("status")?, + created_at: row.try_get("created_at")?, + updated_at: row.try_get("updated_at")?, + })) + } + None => Ok(None), + } + } + + pub async fn update_permission_status(&self, permission_id: &str, status: String) -> Result<()> { + let now = Utc::now(); + + sqlx::query( + "UPDATE permission_requests SET status = ?, updated_at = ? WHERE id = ?" + ) + .bind(&status) + .bind(now) + .bind(permission_id) + .execute(&self.pool) + .await?; + + Ok(()) + } + + pub async fn get_chat_permissions(&self, chat_id: &str) -> Result> { + let rows = sqlx::query( + "SELECT id, chat_id, operation, description, level, details, status, created_at, updated_at FROM permission_requests WHERE chat_id = ? ORDER BY created_at DESC" + ) + .bind(chat_id) + .fetch_all(&self.pool) + .await?; + + let mut permissions = Vec::new(); + for row in rows { + let details_json: String = row.try_get("details")?; + let details = serde_json::from_str(&details_json).unwrap_or_default(); + + permissions.push(crate::models::PermissionRequest { + id: row.try_get("id")?, + chat_id: row.try_get("chat_id")?, + operation: row.try_get("operation")?, + description: row.try_get("description")?, + level: row.try_get("level")?, + details, + status: row.try_get("status")?, + created_at: row.try_get("created_at")?, + updated_at: row.try_get("updated_at")?, + }); + } + + Ok(permissions) + } + // API Configuration operations pub async fn create_api_config(&self, request: CreateApiConfigRequest) -> Result { let id = Uuid::new_v4().to_string(); @@ -359,397 +513,480 @@ impl Database { } // LLM Integration - pub async fn send_chat_completion(&self, config: &ApiConfig, messages: Vec) -> Result { - let client = Client::new(); - - match config.provider { - ApiProvider::OpenAI => { - let url = config.base_url.as_deref().unwrap_or("https://api.openai.com/v1/chat/completions"); - - let request_body = json!({ - "model": config.model, - "messages": messages, - "temperature": config.temperature, - "max_tokens": config.max_tokens - }); - - let response = client - .post(url) - .header("Authorization", format!("Bearer {}", config.api_key)) - .header("Content-Type", "application/json") - .json(&request_body) - .send() - .await?; - - if !response.status().is_success() { - let error_text = response.text().await?; - return Err(anyhow::anyhow!("API request failed: {}", error_text)); - } - // Try to parse as ChatCompletionResponse, but provide better error handling - let response_text = response.text().await?; - - match serde_json::from_str::(&response_text) { - Ok(completion) => { - if let Some(choice) = completion.choices.first() { - // Convert content Value to String - let content_str = match &choice.message.content { - serde_json::Value::String(s) => s.clone(), - other => other.to_string(), - }; - Ok(content_str) - } else { - Err(anyhow::anyhow!("No response choices from API")) - } - }, - Err(parse_error) => { - // Log the actual response for debugging - eprintln!("Failed to parse OpenAI API response: {}", parse_error); - eprintln!("Response body: {}", response_text); - Err(anyhow::anyhow!("Failed to parse API response: {}. Response: {}", parse_error, response_text)) - } - } - }, - ApiProvider::Anthropic => { - let url = config.base_url.as_deref().unwrap_or("https://api.anthropic.com/v1/messages"); - - // Convert messages to Anthropic format - let anthropic_messages: Vec = messages.into_iter().map(|msg| { - json!({ - "role": if msg.role == "assistant" { "assistant" } else { "user" }, - "content": msg.content - }) - }).collect(); - - let request_body = json!({ - "model": config.model, - "max_tokens": config.max_tokens.unwrap_or(1000), - "messages": anthropic_messages - }); - - let response = client - .post(url) - .header("x-api-key", &config.api_key) - .header("anthropic-version", "2023-06-01") - .header("Content-Type", "application/json") - .json(&request_body) - .send() - .await?; - - if !response.status().is_success() { - let error_text = response.text().await?; - return Err(anyhow::anyhow!("Anthropic API request failed: {}", error_text)); - } + // Tool Execution operations + pub async fn create_tool_execution(&self, request: CreateToolExecutionRequest) -> Result { + let id = Uuid::new_v4().to_string(); - let response_json: serde_json::Value = response.json().await?; - - if let Some(content) = response_json["content"][0]["text"].as_str() { - Ok(content.to_string()) - } else { - Err(anyhow::anyhow!("Invalid response format from Anthropic API")) - } - }, - ApiProvider::Ollama => { - let url = format!( - "{}/api/chat", - config.base_url.as_deref().unwrap_or("http://localhost:11434") - ); - - let request_body = json!({ - "model": config.model, - "messages": messages, - "stream": false, - "options": { - "temperature": config.temperature - } - }); - - let response = client - .post(&url) - .header("Content-Type", "application/json") - .json(&request_body) - .send() - .await?; - - if !response.status().is_success() { - let error_text = response.text().await?; - return Err(anyhow::anyhow!("Ollama API request failed: {}", error_text)); - } + // Parse timestamps from ISO strings + let started_at = chrono::DateTime::parse_from_rfc3339(&request.started_at) + .map(|dt| dt.with_timezone(&Utc)) + .unwrap_or_else(|_| Utc::now()); - let response_json: serde_json::Value = response.json().await?; - - if let Some(content) = response_json["message"]["content"].as_str() { - Ok(content.to_string()) - } else { - Err(anyhow::anyhow!("Invalid response format from Ollama API")) - } - }, - ApiProvider::Google => { - let base_url = config.base_url.as_deref().unwrap_or("https://generativelanguage.googleapis.com/v1beta/models"); - - // Check if using OpenAI-compatible endpoint - if base_url.contains("/openai/chat/completions") { - // Use OpenAI-compatible format - let request_body = json!({ - "model": config.model, - "messages": messages, - "temperature": config.temperature, - "max_tokens": config.max_tokens.unwrap_or(1000) - }); - - let response = client - .post(base_url) - .header("Authorization", format!("Bearer {}", config.api_key)) - .header("Content-Type", "application/json") - .json(&request_body) - .send() - .await?; - - if !response.status().is_success() { - let error_text = response.text().await?; - return Err(anyhow::anyhow!("Google OpenAI-compatible API request failed: {}", error_text)); - } - - // Parse OpenAI-compatible response - let response_text = response.text().await?; - - match serde_json::from_str::(&response_text) { - Ok(completion) => { - if let Some(choice) = completion.choices.first() { - // Convert content Value to String - let content_str = match &choice.message.content { - serde_json::Value::String(s) => s.clone(), - other => other.to_string(), - }; - Ok(content_str) - } else { - Err(anyhow::anyhow!("No response choices from Google OpenAI-compatible API")) - } - }, - Err(parse_error) => { - eprintln!("Failed to parse Google OpenAI-compatible API response: {}", parse_error); - eprintln!("Response body: {}", response_text); - Err(anyhow::anyhow!("Failed to parse Google OpenAI-compatible API response: {}. Response: {}", parse_error, response_text)) - } - } - } else { - // Use original Gemini API format - let full_url = format!("{}/{}:generateContent?key={}", base_url, config.model, config.api_key); - - // Convert messages to Google format - let google_contents: Vec = messages.into_iter().map(|msg| { - json!({ - "role": if msg.role == "assistant" { "model" } else { "user" }, - "parts": [{"text": msg.content}] - }) - }).collect(); - - let request_body = json!({ - "contents": google_contents, - "generationConfig": { - "temperature": config.temperature, - "maxOutputTokens": config.max_tokens.unwrap_or(1000) - } - }); - - let response = client - .post(&full_url) - .header("Content-Type", "application/json") - .json(&request_body) - .send() - .await?; - - if !response.status().is_success() { - let error_text = response.text().await?; - return Err(anyhow::anyhow!("Google Gemini API request failed: {}", error_text)); - } - - let response_json: serde_json::Value = response.json().await?; - - if let Some(content) = response_json["candidates"][0]["content"]["parts"][0]["text"].as_str() { - Ok(content.to_string()) - } else { - Err(anyhow::anyhow!("Invalid response format from Google Gemini API")) - } - } - }, - ApiProvider::Custom => { - // For custom providers, assume OpenAI-compatible API format - let url = config.base_url.as_deref().ok_or_else(|| { - anyhow::anyhow!("Base URL is required for custom providers") - })?; - - let request_body = json!({ - "model": config.model, - "messages": messages, - "temperature": config.temperature, - "max_tokens": config.max_tokens - }); - - let mut request_builder = client - .post(url) - .header("Content-Type", "application/json"); - - // Add authorization header if API key is provided - if !config.api_key.is_empty() { - request_builder = request_builder.header("Authorization", format!("Bearer {}", config.api_key)); - } + let completed_at = request.completed_at.as_ref().and_then(|s| { + chrono::DateTime::parse_from_rfc3339(s) + .map(|dt| dt.with_timezone(&Utc)) + .ok() + }); - let response = request_builder - .json(&request_body) - .send() - .await?; + // Serialize JSON fields + let arguments_json = serde_json::to_string(&request.arguments)?; + let result_json = request.result.as_ref().map(|r| serde_json::to_string(r)).transpose()?; - if !response.status().is_success() { - let error_text = response.text().await?; - return Err(anyhow::anyhow!("Custom API request failed: {}", error_text)); - } + sqlx::query( + r#" + INSERT INTO tool_executions ( + id, message_id, tool_call_id, tool_name, tool_source, + arguments, result, success, error_message, execution_order, + started_at, completed_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + "# + ) + .bind(&id) + .bind(&request.message_id) + .bind(&request.tool_call_id) + .bind(&request.tool_name) + .bind(&request.tool_source) + .bind(&arguments_json) + .bind(&result_json) + .bind(request.success) + .bind(&request.error_message) + .bind(request.execution_order) + .bind(started_at) + .bind(completed_at) + .execute(&self.pool) + .await?; - // Try to parse as ChatCompletionResponse, but provide better error handling - let response_text = response.text().await?; - - match serde_json::from_str::(&response_text) { - Ok(completion) => { - if let Some(choice) = completion.choices.first() { - // Convert content Value to String - let content_str = match &choice.message.content { - serde_json::Value::String(s) => s.clone(), - other => other.to_string(), - }; - Ok(content_str) - } else { - Err(anyhow::anyhow!("No response choices from custom API")) - } - }, - Err(parse_error) => { - // Log the actual response for debugging - eprintln!("Failed to parse custom API response: {}", parse_error); - eprintln!("Response body: {}", response_text); - Err(anyhow::anyhow!("Failed to parse custom API response: {}. Response: {}", parse_error, response_text)) - } - } - } + Ok(ToolExecutionRecord { + id, + message_id: request.message_id, + tool_call_id: request.tool_call_id, + tool_name: request.tool_name, + tool_source: request.tool_source, + arguments: request.arguments, + result: request.result, + success: request.success, + error_message: request.error_message, + execution_order: request.execution_order, + started_at, + completed_at, + }) + } + + pub async fn get_tool_executions_for_message(&self, message_id: &str) -> Result> { + let rows = sqlx::query( + r#" + SELECT id, message_id, tool_call_id, tool_name, tool_source, + arguments, result, success, error_message, execution_order, + started_at, completed_at + FROM tool_executions + WHERE message_id = ? + ORDER BY execution_order ASC + "# + ) + .bind(message_id) + .fetch_all(&self.pool) + .await?; + + let mut executions = Vec::new(); + for row in rows { + let arguments_json: String = row.try_get("arguments")?; + let arguments: serde_json::Value = serde_json::from_str(&arguments_json)?; + + let result_json: Option = row.try_get("result")?; + let result: Option = result_json + .map(|s| serde_json::from_str(&s)) + .transpose()?; + + executions.push(ToolExecutionRecord { + id: row.try_get("id")?, + message_id: row.try_get("message_id")?, + tool_call_id: row.try_get("tool_call_id")?, + tool_name: row.try_get("tool_name")?, + tool_source: row.try_get("tool_source")?, + arguments, + result, + success: row.try_get("success")?, + error_message: row.try_get("error_message")?, + execution_order: row.try_get("execution_order")?, + started_at: row.try_get("started_at")?, + completed_at: row.try_get("completed_at")?, + }); } + + Ok(executions) } - pub async fn send_chat_completion_streaming( - &self, - config: &ApiConfig, - messages: Vec, - window: &tauri::Window, - message_id: &str, - chat_id: &str - ) -> Result { - let client = Client::new(); - - match config.provider { - ApiProvider::OpenAI => { - let url = config.base_url.as_deref().unwrap_or("https://api.openai.com/v1/chat/completions"); - - let request_body = json!({ - "model": config.model, - "messages": messages, - "temperature": config.temperature, - "max_tokens": config.max_tokens, - "stream": true - }); - - let response = client - .post(url) - .header("Authorization", format!("Bearer {}", config.api_key)) - .header("Content-Type", "application/json") - .json(&request_body) - .send() - .await?; - - if !response.status().is_success() { - let error_text = response.text().await?; - return Err(anyhow::anyhow!("API request failed: {}", error_text)); - } + pub async fn get_tool_executions_for_messages(&self, message_ids: &[String]) -> Result>> { + if message_ids.is_empty() { + return Ok(std::collections::HashMap::new()); + } - let mut full_response = String::new(); - let mut stream = response.bytes_stream(); - - use futures_util::StreamExt; - - while let Some(chunk) = stream.next().await { - let chunk = chunk?; - let chunk_str = String::from_utf8_lossy(&chunk); - - // Parse SSE format - for line in chunk_str.lines() { - if line.starts_with("data: ") { - let data = &line[6..]; - if data == "[DONE]" { - break; - } - - if let Ok(json_data) = serde_json::from_str::(data) { - if let Some(choices) = json_data["choices"].as_array() { - if let Some(choice) = choices.first() { - if let Some(delta) = choice["delta"].as_object() { - if let Some(content) = delta["content"].as_str() { - full_response.push_str(content); - - // Emit streaming chunk to frontend - let _ = window.emit("streaming_chunk", serde_json::json!({ - "message_id": message_id, - "chunk": content, - "full_content": full_response - })); - } - } - } - } - } - } - } - } + // Build placeholders for IN clause + let placeholders: Vec = message_ids.iter().map(|_| "?".to_string()).collect(); + let query = format!( + r#" + SELECT id, message_id, tool_call_id, tool_name, tool_source, + arguments, result, success, error_message, execution_order, + started_at, completed_at + FROM tool_executions + WHERE message_id IN ({}) + ORDER BY message_id, execution_order ASC + "#, + placeholders.join(", ") + ); + + let mut query_builder = sqlx::query(&query); + for id in message_ids { + query_builder = query_builder.bind(id); + } - // Emit streaming complete event with the content - let _ = window.emit("streaming_complete", serde_json::json!({ - "message_id": message_id, - "content": full_response, - "chat_id": chat_id - })); - - Ok(full_response) - }, - // For other providers, fall back to non-streaming for now - _ => { - // Simulate streaming by sending the full response in chunks - let response = self.send_chat_completion(config, messages).await?; - - // Split response into words and send as chunks - let words: Vec<&str> = response.split_whitespace().collect(); - let mut current_content = String::new(); - - for (i, word) in words.iter().enumerate() { - current_content.push_str(word); - if i < words.len() - 1 { - current_content.push(' '); - } - - // Emit chunk - let _ = window.emit("streaming_chunk", serde_json::json!({ - "message_id": message_id, - "chunk": format!("{} ", word), - "full_content": current_content - })); - - // Small delay to simulate streaming - tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; - } - - // Emit streaming complete event with the content - let _ = window.emit("streaming_complete", serde_json::json!({ - "message_id": message_id, - "content": response, - "chat_id": chat_id - })); - - Ok(response) + let rows = query_builder.fetch_all(&self.pool).await?; + + let mut result_map: std::collections::HashMap> = std::collections::HashMap::new(); + + for row in rows { + let arguments_json: String = row.try_get("arguments")?; + let arguments: serde_json::Value = serde_json::from_str(&arguments_json)?; + + let result_json: Option = row.try_get("result")?; + let result: Option = result_json + .map(|s| serde_json::from_str(&s)) + .transpose()?; + + let message_id: String = row.try_get("message_id")?; + let execution = ToolExecutionRecord { + id: row.try_get("id")?, + message_id: message_id.clone(), + tool_call_id: row.try_get("tool_call_id")?, + tool_name: row.try_get("tool_name")?, + tool_source: row.try_get("tool_source")?, + arguments, + result, + success: row.try_get("success")?, + error_message: row.try_get("error_message")?, + execution_order: row.try_get("execution_order")?, + started_at: row.try_get("started_at")?, + completed_at: row.try_get("completed_at")?, + }; + + result_map.entry(message_id).or_insert_with(Vec::new).push(execution); + } + + Ok(result_map) + } + + // MCP Server operations + pub async fn create_mcp_server(&self, request: CreateMcpServerRequest) -> Result { + let id = Uuid::new_v4().to_string(); + let now = Utc::now(); + + // Serialize JSON fields + let args_json = request.args.as_ref().map(|a| serde_json::to_string(a)).transpose()?; + let env_json = request.env.as_ref().map(|e| serde_json::to_string(e)).transpose()?; + let headers_json = request.headers.as_ref().map(|h| serde_json::to_string(h)).transpose()?; + + sqlx::query( + r#" + INSERT INTO mcp_servers ( + id, name, transport_type, command, args, env, url, headers, + enabled, auto_connect, connection_timeout_ms, created_at, updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + "# + ) + .bind(&id) + .bind(&request.name) + .bind(&request.transport_type) + .bind(&request.command) + .bind(&args_json) + .bind(&env_json) + .bind(&request.url) + .bind(&headers_json) + .bind(request.enabled) + .bind(request.auto_connect) + .bind(request.connection_timeout_ms) + .bind(now) + .bind(now) + .execute(&self.pool) + .await?; + + Ok(McpServer { + id, + name: request.name, + transport_type: request.transport_type, + command: request.command, + args: request.args, + env: request.env, + url: request.url, + headers: request.headers, + enabled: request.enabled, + auto_connect: request.auto_connect, + connection_timeout_ms: request.connection_timeout_ms, + created_at: now, + updated_at: now, + }) + } + + pub async fn get_mcp_servers(&self) -> Result> { + let rows = sqlx::query( + "SELECT * FROM mcp_servers ORDER BY name ASC" + ) + .fetch_all(&self.pool) + .await?; + + let mut servers = Vec::new(); + for row in rows { + let transport_type_str: String = row.try_get("transport_type")?; + let transport_type = match transport_type_str.as_str() { + "stdio" => McpTransportType::Stdio, + "sse" => McpTransportType::Sse, + _ => return Err(anyhow::anyhow!("Invalid transport type: {}", transport_type_str)), + }; + + let args: Option> = row.try_get::, _>("args")? + .and_then(|s| serde_json::from_str(&s).ok()); + let env: Option> = row.try_get::, _>("env")? + .and_then(|s| serde_json::from_str(&s).ok()); + let headers: Option> = row.try_get::, _>("headers")? + .and_then(|s| serde_json::from_str(&s).ok()); + + servers.push(McpServer { + id: row.try_get("id")?, + name: row.try_get("name")?, + transport_type, + command: row.try_get("command")?, + args, + env, + url: row.try_get("url")?, + headers, + enabled: row.try_get("enabled")?, + auto_connect: row.try_get("auto_connect")?, + connection_timeout_ms: row.try_get("connection_timeout_ms")?, + created_at: row.try_get("created_at")?, + updated_at: row.try_get("updated_at")?, + }); + } + + Ok(servers) + } + + pub async fn get_mcp_server(&self, server_id: &str) -> Result> { + let row = sqlx::query("SELECT * FROM mcp_servers WHERE id = ?") + .bind(server_id) + .fetch_optional(&self.pool) + .await?; + + match row { + Some(row) => { + let transport_type_str: String = row.try_get("transport_type")?; + let transport_type = match transport_type_str.as_str() { + "stdio" => McpTransportType::Stdio, + "sse" => McpTransportType::Sse, + _ => return Err(anyhow::anyhow!("Invalid transport type: {}", transport_type_str)), + }; + + let args: Option> = row.try_get::, _>("args")? + .and_then(|s| serde_json::from_str(&s).ok()); + let env: Option> = row.try_get::, _>("env")? + .and_then(|s| serde_json::from_str(&s).ok()); + let headers: Option> = row.try_get::, _>("headers")? + .and_then(|s| serde_json::from_str(&s).ok()); + + Ok(Some(McpServer { + id: row.try_get("id")?, + name: row.try_get("name")?, + transport_type, + command: row.try_get("command")?, + args, + env, + url: row.try_get("url")?, + headers, + enabled: row.try_get("enabled")?, + auto_connect: row.try_get("auto_connect")?, + connection_timeout_ms: row.try_get("connection_timeout_ms")?, + created_at: row.try_get("created_at")?, + updated_at: row.try_get("updated_at")?, + })) } + None => Ok(None), } } + + pub async fn update_mcp_server(&self, server_id: &str, request: UpdateMcpServerRequest) -> Result { + let now = Utc::now(); + + let args_json = request.args.as_ref().map(|a| serde_json::to_string(a)).transpose()?; + let env_json = request.env.as_ref().map(|e| serde_json::to_string(e)).transpose()?; + let headers_json = request.headers.as_ref().map(|h| serde_json::to_string(h)).transpose()?; + + sqlx::query( + r#" + UPDATE mcp_servers SET + name = ?, transport_type = ?, command = ?, args = ?, env = ?, + url = ?, headers = ?, enabled = ?, auto_connect = ?, + connection_timeout_ms = ?, updated_at = ? + WHERE id = ? + "# + ) + .bind(&request.name) + .bind(&request.transport_type) + .bind(&request.command) + .bind(&args_json) + .bind(&env_json) + .bind(&request.url) + .bind(&headers_json) + .bind(request.enabled) + .bind(request.auto_connect) + .bind(request.connection_timeout_ms) + .bind(now) + .bind(server_id) + .execute(&self.pool) + .await?; + + Ok(McpServer { + id: server_id.to_string(), + name: request.name, + transport_type: request.transport_type, + command: request.command, + args: request.args, + env: request.env, + url: request.url, + headers: request.headers, + enabled: request.enabled, + auto_connect: request.auto_connect, + connection_timeout_ms: request.connection_timeout_ms, + created_at: now, // Will be overwritten by actual value + updated_at: now, + }) + } + + pub async fn delete_mcp_server(&self, server_id: &str) -> Result<()> { + // Tools will be cascade deleted + sqlx::query("DELETE FROM mcp_servers WHERE id = ?") + .bind(server_id) + .execute(&self.pool) + .await?; + + Ok(()) + } + + // MCP Tool operations + pub async fn sync_mcp_tools(&self, server_id: &str, tools: Vec<(String, Option, serde_json::Value)>) -> Result> { + let now = Utc::now(); + + // Delete existing tools for this server + sqlx::query("DELETE FROM mcp_tools WHERE server_id = ?") + .bind(server_id) + .execute(&self.pool) + .await?; + + let mut result_tools = Vec::new(); + + for (name, description, input_schema) in tools { + let id = Uuid::new_v4().to_string(); + let schema_json = serde_json::to_string(&input_schema)?; + + sqlx::query( + r#" + INSERT INTO mcp_tools (id, server_id, name, description, input_schema, enabled, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, 1, ?, ?) + "# + ) + .bind(&id) + .bind(server_id) + .bind(&name) + .bind(&description) + .bind(&schema_json) + .bind(now) + .bind(now) + .execute(&self.pool) + .await?; + + result_tools.push(McpTool { + id, + server_id: server_id.to_string(), + name, + description, + input_schema, + enabled: true, + created_at: now, + updated_at: now, + }); + } + + Ok(result_tools) + } + + pub async fn get_mcp_tools_for_server(&self, server_id: &str) -> Result> { + let rows = sqlx::query( + "SELECT * FROM mcp_tools WHERE server_id = ? ORDER BY name ASC" + ) + .bind(server_id) + .fetch_all(&self.pool) + .await?; + + let mut tools = Vec::new(); + for row in rows { + let schema_json: String = row.try_get("input_schema")?; + let input_schema: serde_json::Value = serde_json::from_str(&schema_json)?; + + tools.push(McpTool { + id: row.try_get("id")?, + server_id: row.try_get("server_id")?, + name: row.try_get("name")?, + description: row.try_get("description")?, + input_schema, + enabled: row.try_get("enabled")?, + created_at: row.try_get("created_at")?, + updated_at: row.try_get("updated_at")?, + }); + } + + Ok(tools) + } + + pub async fn get_enabled_mcp_tools(&self) -> Result> { + let rows = sqlx::query( + r#" + SELECT t.* FROM mcp_tools t + INNER JOIN mcp_servers s ON t.server_id = s.id + WHERE t.enabled = 1 AND s.enabled = 1 + ORDER BY s.name, t.name + "# + ) + .fetch_all(&self.pool) + .await?; + + let mut tools = Vec::new(); + for row in rows { + let schema_json: String = row.try_get("input_schema")?; + let input_schema: serde_json::Value = serde_json::from_str(&schema_json)?; + + tools.push(McpTool { + id: row.try_get("id")?, + server_id: row.try_get("server_id")?, + name: row.try_get("name")?, + description: row.try_get("description")?, + input_schema, + enabled: row.try_get("enabled")?, + created_at: row.try_get("created_at")?, + updated_at: row.try_get("updated_at")?, + }); + } + + Ok(tools) + } + + pub async fn toggle_mcp_tool(&self, tool_id: &str, enabled: bool) -> Result<()> { + let now = Utc::now(); + + sqlx::query("UPDATE mcp_tools SET enabled = ?, updated_at = ? WHERE id = ?") + .bind(enabled) + .bind(now) + .bind(tool_id) + .execute(&self.pool) + .await?; + + Ok(()) + } } \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5a2c5b7..bf1c8ff 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,10 +4,18 @@ mod models; mod file_operations; mod agentic; mod system_operations; +mod tools; +mod llm_streaming; +mod permission_manager; +mod mcp_client; +mod tool_registry; use database::Database; +use permission_manager::PermissionManager; +use mcp_client::McpClientManager; +use tool_registry::ToolRegistry; use std::collections::HashMap; -use std::sync::Mutex; +use std::sync::{Arc, Mutex}; use agentic::AgentSession; #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -15,11 +23,17 @@ pub fn run() { tauri::async_runtime::block_on(async { let db = Database::new().await.expect("Failed to initialize database"); let agent_sessions: Mutex> = Mutex::new(HashMap::new()); + let permission_manager = PermissionManager::new(); + let mcp_manager = Arc::new(McpClientManager::new()); + let tool_registry = Arc::new(ToolRegistry::new(Arc::clone(&mcp_manager))); tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .manage(db) .manage(agent_sessions) + .manage(permission_manager) + .manage(mcp_manager) + .manage(tool_registry) .invoke_handler(tauri::generate_handler![ commands::create_chat, commands::get_chats, @@ -35,8 +49,10 @@ pub fn run() { commands::get_default_api_config, commands::update_api_config, commands::delete_api_config, - commands::send_ai_message, - commands::send_ai_message_streaming, + // Tool executions + commands::create_tool_execution, + commands::get_tool_executions_for_message, + commands::get_tool_executions_for_messages, // File operations commands::open_file_with_default_app, commands::read_directory, @@ -47,17 +63,43 @@ pub fn run() { // Agentic mode commands::create_agent_session, commands::get_agent_capabilities, + commands::get_agent_tool_definitions, commands::execute_agent_action, commands::get_agent_session, commands::create_or_get_agent_session, // System operations with permissions commands::request_permission, + commands::respond_to_permission, + commands::clear_chat_permissions, + commands::clear_permission, + commands::get_chat_permissions, commands::launch_app, commands::get_installed_apps, commands::execute_command, commands::perform_file_system_operation, commands::get_processes, commands::terminate_process, + // LLM operations + commands::fetch_provider_models, + commands::stream_llm_request, + // System info + commands::get_system_info, + // MCP server management + commands::create_mcp_server, + commands::get_mcp_servers, + commands::get_mcp_server, + commands::update_mcp_server, + commands::delete_mcp_server, + commands::get_mcp_tools_for_server, + commands::get_enabled_mcp_tools, + commands::toggle_mcp_tool, + // MCP connection management + commands::connect_mcp_server, + commands::disconnect_mcp_server, + commands::get_mcp_server_status, + commands::get_all_mcp_statuses, + commands::get_merged_tool_definitions, + commands::execute_tool_routed, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/llm_streaming.rs b/src-tauri/src/llm_streaming.rs new file mode 100644 index 0000000..6c3949a --- /dev/null +++ b/src-tauri/src/llm_streaming.rs @@ -0,0 +1,693 @@ +use futures_util::StreamExt; +use reqwest; +use serde_json; +use std::collections::HashMap; +use tauri::{Emitter, Window}; + +/// Stream an OpenAI-compatible chat completion request +pub async fn stream_openai_compatible( + window: &Window, + base_url: &str, + api_key: &str, + model: &str, + messages: Vec, + tools: Option>, + temperature: f32, + max_tokens: Option, + stream_id: &str, +) -> Result<(), String> { + let client = reqwest::Client::new(); + + let mut body = serde_json::json!({ + "model": model, + "messages": messages, + "temperature": temperature, + "stream": true, + }); + + if let Some(tools) = tools { + body["tools"] = serde_json::json!(tools); + } + + if let Some(max_tokens) = max_tokens { + body["max_tokens"] = serde_json::json!(max_tokens); + } + + let response = client + .post(format!("{}/chat/completions", base_url)) + .header("Authorization", format!("Bearer {}", api_key)) + .header("Content-Type", "application/json") + .json(&body) + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await.unwrap_or_default(); + return Err(format!("HTTP {}: {}", status, error_text)); + } + + let mut stream = response.bytes_stream(); + let mut buffer = String::new(); + let mut full_content = String::new(); + let mut tool_calls: Vec = Vec::new(); + let mut tool_calls_map: HashMap = HashMap::new(); + + window + .emit(&format!("streaming_start_{}", stream_id), ()) + .map_err(|e| format!("Failed to emit streaming_start: {}", e))?; + + while let Some(chunk) = stream.next().await { + let chunk = chunk.map_err(|e| format!("Stream error: {}", e))?; + buffer.push_str(&String::from_utf8_lossy(&chunk)); + + // Process complete lines + while let Some(newline_pos) = buffer.find('\n') { + let line = buffer[..newline_pos].trim().to_string(); + buffer = buffer[newline_pos + 1..].to_string(); + + if line.is_empty() || !line.starts_with("data: ") { + continue; + } + + let data = &line[6..]; + if data == "[DONE]" { + break; + } + + let json: serde_json::Value = match serde_json::from_str(data) { + Ok(j) => j, + Err(_) => continue, // Skip malformed JSON + }; + + if let Some(choices) = json["choices"].as_array() { + for choice in choices { + let delta = &choice["delta"]; + + // Handle content + if let Some(content) = delta["content"].as_str() { + full_content.push_str(content); + window + .emit( + &format!("streaming_chunk_{}", stream_id), + serde_json::json!({ + "chunk": content, + "full_content": full_content + }), + ) + .map_err(|e| format!("Failed to emit streaming_chunk: {}", e))?; + } + + // Handle tool calls + if let Some(tool_calls_delta) = delta["tool_calls"].as_array() { + for tc in tool_calls_delta { + let index = tc["index"].as_u64().unwrap_or(0) as usize; + + if !tool_calls_map.contains_key(&index) { + tool_calls_map.insert( + index, + serde_json::json!({ + "id": tc["id"].as_str().unwrap_or(""), + "type": "function", + "function": { + "name": tc["function"]["name"].as_str().unwrap_or(""), + "arguments": tc["function"]["arguments"].as_str().unwrap_or("") + } + }), + ); + } else { + let existing = tool_calls_map.get_mut(&index).unwrap(); + if let Some(name) = tc["function"]["name"].as_str() { + existing["function"]["name"] = serde_json::json!(name); + } + if let Some(args) = tc["function"]["arguments"].as_str() { + let current_args = + existing["function"]["arguments"].as_str().unwrap_or(""); + existing["function"]["arguments"] = + serde_json::json!(format!("{}{}", current_args, args)); + } + if let Some(id) = tc["id"].as_str() { + existing["id"] = serde_json::json!(id); + } + } + } + } + + // Handle finish_reason + if let Some(finish_reason) = choice["finish_reason"].as_str() { + if finish_reason == "tool_calls" { + tool_calls = tool_calls_map.values().cloned().collect(); + } + } + } + } + } + } + + // Emit tool calls if present + if !tool_calls.is_empty() { + window + .emit( + &format!("streaming_tool_calls_{}", stream_id), + serde_json::json!({ + "tool_calls": tool_calls + }), + ) + .map_err(|e| format!("Failed to emit streaming_tool_calls: {}", e))?; + } + + window + .emit(&format!("streaming_complete_{}", stream_id), ()) + .map_err(|e| format!("Failed to emit streaming_complete: {}", e))?; + + Ok(()) +} + +/// Stream an Anthropic chat completion request +pub async fn stream_anthropic( + window: &Window, + api_key: &str, + model: &str, + messages: Vec, + tools: Option>, + temperature: f32, + max_tokens: Option, + stream_id: &str, +) -> Result<(), String> { + let client = reqwest::Client::new(); + + // Convert OpenAI message format to Anthropic format + let mut anthropic_messages: Vec = Vec::new(); + let mut system_message: Option = None; + let mut pending_tool_results: Vec = Vec::new(); + + for msg in &messages { + if msg["role"] == "system" { + system_message = msg["content"].as_str().map(|s| s.to_string()); + } else if msg["role"] == "user" { + // If we have pending tool results, add them as a user message first + if !pending_tool_results.is_empty() { + anthropic_messages.push(serde_json::json!({ + "role": "user", + "content": pending_tool_results.clone() + })); + pending_tool_results.clear(); + } + + // Handle multimodal content + if msg["content"].is_array() { + anthropic_messages.push(serde_json::json!({ + "role": "user", + "content": msg["content"] + })); + } else { + anthropic_messages.push(serde_json::json!({ + "role": "user", + "content": msg["content"] + })); + } + } else if msg["role"] == "assistant" { + // If we have pending tool results, add them as a user message first + if !pending_tool_results.is_empty() { + anthropic_messages.push(serde_json::json!({ + "role": "user", + "content": pending_tool_results.clone() + })); + pending_tool_results.clear(); + } + + // Check if this assistant message has tool_calls + if let Some(tool_calls) = msg["tool_calls"].as_array() { + // Convert to Anthropic format with tool_use content blocks + let mut content_blocks: Vec = Vec::new(); + + // Add text content if present + if let Some(text) = msg["content"].as_str() { + if !text.is_empty() { + content_blocks.push(serde_json::json!({ + "type": "text", + "text": text + })); + } + } + + // Add tool_use blocks + for tc in tool_calls { + let args_str = tc["function"]["arguments"].as_str().unwrap_or("{}"); + let args: serde_json::Value = serde_json::from_str(args_str).unwrap_or(serde_json::json!({})); + + content_blocks.push(serde_json::json!({ + "type": "tool_use", + "id": tc["id"], + "name": tc["function"]["name"], + "input": args + })); + } + + anthropic_messages.push(serde_json::json!({ + "role": "assistant", + "content": content_blocks + })); + } else { + // Regular assistant message + anthropic_messages.push(serde_json::json!({ + "role": "assistant", + "content": msg["content"] + })); + } + } else if msg["role"] == "tool" { + // Collect tool results to send as a user message with tool_result content blocks + let tool_call_id = msg["tool_call_id"].as_str().unwrap_or(""); + let content = if msg["content"].is_string() { + msg["content"].as_str().unwrap_or("").to_string() + } else { + serde_json::to_string(&msg["content"]).unwrap_or_default() + }; + + pending_tool_results.push(serde_json::json!({ + "type": "tool_result", + "tool_use_id": tool_call_id, + "content": content + })); + } + } + + // Add any remaining tool results + if !pending_tool_results.is_empty() { + anthropic_messages.push(serde_json::json!({ + "role": "user", + "content": pending_tool_results + })); + } + + let mut body = serde_json::json!({ + "model": model, + "messages": anthropic_messages, + "temperature": temperature, + "max_tokens": max_tokens.unwrap_or(4096), + "stream": true, + }); + + if let Some(system) = system_message { + body["system"] = serde_json::json!(system); + } + + if let Some(tools) = tools { + // Convert OpenAI tool format to Anthropic format + let anthropic_tools: Vec = tools + .iter() + .map(|t| { + serde_json::json!({ + "name": t["function"]["name"], + "description": t["function"]["description"], + "input_schema": t["function"]["parameters"] + }) + }) + .collect(); + body["tools"] = serde_json::json!(anthropic_tools); + } + + let response = client + .post("https://api.anthropic.com/v1/messages") + .header("x-api-key", api_key) + .header("anthropic-version", "2023-06-01") + .header("Content-Type", "application/json") + .json(&body) + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await.unwrap_or_default(); + return Err(format!("HTTP {}: {}", status, error_text)); + } + + let mut stream = response.bytes_stream(); + let mut buffer = String::new(); + let mut full_content = String::new(); + let mut tool_calls: Vec = Vec::new(); + // Track current tool use being streamed (index -> (id, name, accumulated_input)) + let mut current_tool_uses: HashMap = HashMap::new(); + + window + .emit(&format!("streaming_start_{}", stream_id), ()) + .map_err(|e| format!("Failed to emit streaming_start: {}", e))?; + + while let Some(chunk) = stream.next().await { + let chunk = chunk.map_err(|e| format!("Stream error: {}", e))?; + buffer.push_str(&String::from_utf8_lossy(&chunk)); + + while let Some(newline_pos) = buffer.find('\n') { + let line = buffer[..newline_pos].trim().to_string(); + buffer = buffer[newline_pos + 1..].to_string(); + + if line.is_empty() || !line.starts_with("data: ") { + continue; + } + + let data = &line[6..]; + if data == "[DONE]" { + break; + } + + let json: serde_json::Value = match serde_json::from_str(data) { + Ok(j) => j, + Err(_) => continue, + }; + + // Handle content delta (text) + if json["type"] == "content_block_delta" { + let index = json["index"].as_u64().unwrap_or(0) as usize; + + // Text delta + if let Some(text) = json["delta"]["text"].as_str() { + full_content.push_str(text); + window + .emit( + &format!("streaming_chunk_{}", stream_id), + serde_json::json!({ + "chunk": text, + "full_content": full_content + }), + ) + .map_err(|e| format!("Failed to emit streaming_chunk: {}", e))?; + } + + // Input JSON delta for tool use + if let Some(partial_json) = json["delta"]["partial_json"].as_str() { + if let Some((_, _, ref mut accumulated)) = current_tool_uses.get_mut(&index) { + accumulated.push_str(partial_json); + } + } + } + + // Handle tool use start + if json["type"] == "content_block_start" { + let index = json["index"].as_u64().unwrap_or(0) as usize; + if json["content_block"]["type"] == "tool_use" { + let tool_use = &json["content_block"]; + let id = tool_use["id"].as_str().unwrap_or("").to_string(); + let name = tool_use["name"].as_str().unwrap_or("").to_string(); + current_tool_uses.insert(index, (id, name, String::new())); + } + } + + // Handle content block stop - finalize tool use + if json["type"] == "content_block_stop" { + let index = json["index"].as_u64().unwrap_or(0) as usize; + if let Some((id, name, accumulated_input)) = current_tool_uses.remove(&index) { + // Only add if we have a valid tool (has id and name) + if !id.is_empty() && !name.is_empty() { + tool_calls.push(serde_json::json!({ + "id": id, + "type": "function", + "function": { + "name": name, + "arguments": if accumulated_input.is_empty() { "{}".to_string() } else { accumulated_input } + } + })); + } + } + } + } + } + + if !tool_calls.is_empty() { + window + .emit( + &format!("streaming_tool_calls_{}", stream_id), + serde_json::json!({ + "tool_calls": tool_calls + }), + ) + .map_err(|e| format!("Failed to emit streaming_tool_calls: {}", e))?; + } + + window + .emit(&format!("streaming_complete_{}", stream_id), ()) + .map_err(|e| format!("Failed to emit streaming_complete: {}", e))?; + + Ok(()) +} + +/// Stream a Google Gemini chat completion request +pub async fn stream_google( + window: &Window, + api_key: &str, + model: &str, + messages: Vec, + tools: Option>, + temperature: f32, + stream_id: &str, +) -> Result<(), String> { + let client = reqwest::Client::new(); + + // Convert messages to Google format + let mut google_contents: Vec = Vec::new(); + let mut system_instruction: Option = None; + let mut pending_function_responses: Vec = Vec::new(); + + for msg in &messages { + if msg["role"] == "system" { + system_instruction = msg["content"].as_str().map(|s| s.to_string()); + } else if msg["role"] == "user" { + // If we have pending function responses, add them first + if !pending_function_responses.is_empty() { + google_contents.push(serde_json::json!({ + "role": "user", + "parts": pending_function_responses.clone() + })); + pending_function_responses.clear(); + } + + // Handle multimodal content + if msg["content"].is_array() { + google_contents.push(serde_json::json!({ + "role": "user", + "parts": msg["content"] + })); + } else { + google_contents.push(serde_json::json!({ + "role": "user", + "parts": [{"text": msg["content"]}] + })); + } + } else if msg["role"] == "assistant" { + // If we have pending function responses, add them first + if !pending_function_responses.is_empty() { + google_contents.push(serde_json::json!({ + "role": "user", + "parts": pending_function_responses.clone() + })); + pending_function_responses.clear(); + } + + // Check if this assistant message has tool_calls + if let Some(tool_calls) = msg["tool_calls"].as_array() { + let mut parts: Vec = Vec::new(); + + // Add text content if present + if let Some(text) = msg["content"].as_str() { + if !text.is_empty() { + parts.push(serde_json::json!({"text": text})); + } + } + + // Add functionCall parts + for tc in tool_calls { + let args_str = tc["function"]["arguments"].as_str().unwrap_or("{}"); + let args: serde_json::Value = serde_json::from_str(args_str).unwrap_or(serde_json::json!({})); + + parts.push(serde_json::json!({ + "functionCall": { + "name": tc["function"]["name"], + "args": args + } + })); + } + + google_contents.push(serde_json::json!({ + "role": "model", + "parts": parts + })); + } else { + // Regular assistant message + google_contents.push(serde_json::json!({ + "role": "model", + "parts": [{"text": msg["content"]}] + })); + } + } else if msg["role"] == "tool" { + // Collect function responses to send as a user message + let tool_name = msg["name"].as_str().unwrap_or(""); + let content = if msg["content"].is_string() { + serde_json::from_str(msg["content"].as_str().unwrap_or("{}")).unwrap_or(serde_json::json!({})) + } else { + msg["content"].clone() + }; + + pending_function_responses.push(serde_json::json!({ + "functionResponse": { + "name": tool_name, + "response": content + } + })); + } + } + + // Add any remaining function responses + if !pending_function_responses.is_empty() { + google_contents.push(serde_json::json!({ + "role": "user", + "parts": pending_function_responses + })); + } + + let mut body = serde_json::json!({ + "contents": google_contents, + "generationConfig": { + "temperature": temperature, + } + }); + + if let Some(system) = system_instruction { + body["systemInstruction"] = serde_json::json!({ + "parts": [{"text": system}] + }); + } + + if let Some(tools) = tools { + let google_tools: Vec = tools + .iter() + .map(|t| { + serde_json::json!({ + "name": t["function"]["name"], + "description": t["function"]["description"], + "parameters": t["function"]["parameters"] + }) + }) + .collect(); + body["tools"] = serde_json::json!([{ + "functionDeclarations": google_tools + }]); + } + + let url = format!( + "https://generativelanguage.googleapis.com/v1beta/models/{}:streamGenerateContent?key={}", + model, api_key + ); + + let response = client + .post(&url) + .header("Content-Type", "application/json") + .json(&body) + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + if !response.status().is_success() { + let status = response.status(); + let error_text = response.text().await.unwrap_or_default(); + return Err(format!("HTTP {}: {}", status, error_text)); + } + + let mut stream = response.bytes_stream(); + let mut buffer = String::new(); + let mut full_content = String::new(); + let mut tool_calls: Vec = Vec::new(); + + window + .emit(&format!("streaming_start_{}", stream_id), ()) + .map_err(|e| format!("Failed to emit streaming_start: {}", e))?; + + while let Some(chunk) = stream.next().await { + let chunk = chunk.map_err(|e| format!("Stream error: {}", e))?; + buffer.push_str(&String::from_utf8_lossy(&chunk)); + + while let Some(newline_pos) = buffer.find('\n') { + let line = buffer[..newline_pos].trim().to_string(); + buffer = buffer[newline_pos + 1..].to_string(); + + if line.is_empty() { + continue; + } + + let json: serde_json::Value = match serde_json::from_str(&line) { + Ok(j) => j, + Err(_) => continue, + }; + + if let Some(candidates) = json["candidates"].as_array() { + for candidate in candidates { + if let Some(content) = candidate["content"].as_object() { + if let Some(parts) = content["parts"].as_array() { + for part in parts { + // Handle text content + if let Some(text) = part["text"].as_str() { + full_content.push_str(text); + window + .emit( + &format!("streaming_chunk_{}", stream_id), + serde_json::json!({ + "chunk": text, + "full_content": full_content + }), + ) + .map_err(|e| format!("Failed to emit streaming_chunk: {}", e))?; + } + + // Handle function calls + if let Some(function_call) = part["functionCall"].as_object() { + tool_calls.push(serde_json::json!({ + "id": format!("google-{}", tool_calls.len()), + "type": "function", + "function": { + "name": function_call["name"], + "arguments": serde_json::to_string(&function_call["args"]).unwrap_or_default() + } + })); + } + } + } + } + } + } + } + } + + if !tool_calls.is_empty() { + window + .emit( + &format!("streaming_tool_calls_{}", stream_id), + serde_json::json!({ + "tool_calls": tool_calls + }), + ) + .map_err(|e| format!("Failed to emit streaming_tool_calls: {}", e))?; + } + + window + .emit(&format!("streaming_complete_{}", stream_id), ()) + .map_err(|e| format!("Failed to emit streaming_complete: {}", e))?; + + Ok(()) +} + +/// Get default base URL for a provider +pub fn get_default_base_url(provider: &str) -> String { + match provider { + "openai" => "https://api.openai.com/v1".to_string(), + "deepseek" => "https://api.deepseek.com/v1".to_string(), + "mistral" => "https://api.mistral.ai/v1".to_string(), + "lmstudio" => "http://localhost:1234/v1".to_string(), + "kimi" => "https://api.moonshot.cn/v1".to_string(), + "openrouter" => "https://openrouter.ai/api/v1".to_string(), + "together" => "https://api.together.xyz/v1".to_string(), + "groq" => "https://api.groq.com/openai/v1".to_string(), + "perplexity" => "https://api.perplexity.ai".to_string(), + "ollama" => "http://localhost:11434/v1".to_string(), + _ => "https://api.openai.com/v1".to_string(), + } +} diff --git a/src-tauri/src/mcp_client.rs b/src-tauri/src/mcp_client.rs new file mode 100644 index 0000000..520746f --- /dev/null +++ b/src-tauri/src/mcp_client.rs @@ -0,0 +1,554 @@ +use anyhow::{anyhow, Result}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::collections::HashMap; +use std::process::Stdio; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::Arc; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::process::{Child, Command}; +use tokio::sync::{mpsc, Mutex, RwLock}; +use tokio::time::{timeout, Duration}; + +use crate::models::{McpServer, McpTransportType}; + +// JSON-RPC 2.0 structures +#[derive(Debug, Serialize, Deserialize)] +struct JsonRpcRequest { + jsonrpc: String, + id: u64, + method: String, + #[serde(skip_serializing_if = "Option::is_none")] + params: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct JsonRpcResponse { + jsonrpc: String, + id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct JsonRpcError { + code: i32, + message: String, + #[serde(skip_serializing_if = "Option::is_none")] + data: Option, +} + +// MCP Protocol structures +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct McpToolInfo { + pub name: String, + #[serde(default)] + pub description: Option, + #[serde(rename = "inputSchema")] + pub input_schema: Value, +} + +#[derive(Debug, Serialize, Deserialize)] +struct InitializeParams { + #[serde(rename = "protocolVersion")] + protocol_version: String, + capabilities: ClientCapabilities, + #[serde(rename = "clientInfo")] + client_info: ClientInfo, +} + +#[derive(Debug, Serialize, Deserialize)] +struct ClientCapabilities { + // Empty for now, can be extended +} + +#[derive(Debug, Serialize, Deserialize)] +struct ClientInfo { + name: String, + version: String, +} + +#[derive(Debug, Serialize, Deserialize)] +struct InitializeResult { + #[serde(rename = "protocolVersion")] + protocol_version: String, + capabilities: ServerCapabilities, + #[serde(rename = "serverInfo")] + server_info: Option, +} + +#[derive(Debug, Serialize, Deserialize, Default)] +struct ServerCapabilities { + tools: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct ToolsCapability { + #[serde(rename = "listChanged")] + list_changed: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct ServerInfo { + name: String, + version: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct ListToolsResult { + tools: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +struct CallToolParams { + name: String, + #[serde(skip_serializing_if = "Option::is_none")] + arguments: Option, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ToolCallResult { + pub content: Vec, + #[serde(rename = "isError", default)] + pub is_error: bool, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(tag = "type")] +pub enum ToolContent { + #[serde(rename = "text")] + Text { text: String }, + #[serde(rename = "image")] + Image { data: String, #[serde(rename = "mimeType")] mime_type: String }, + #[serde(rename = "resource")] + Resource { resource: ResourceContent }, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ResourceContent { + pub uri: String, + #[serde(rename = "mimeType")] + pub mime_type: Option, + pub text: Option, +} + +// Connection state +#[derive(Debug, Clone, PartialEq)] +pub enum ConnectionStatus { + Disconnected, + Connecting, + Connected, + Error(String), +} + +// MCP Client for a single server +pub struct McpClient { + server_id: String, + config: McpServer, + status: Arc>, + request_id: AtomicU64, + // For stdio transport + child_process: Arc>>, + stdin_tx: Arc>>>, + pending_requests: Arc>>>>, + // Discovered tools + tools: Arc>>, +} + +impl McpClient { + pub fn new(config: McpServer) -> Self { + Self { + server_id: config.id.clone(), + config, + status: Arc::new(RwLock::new(ConnectionStatus::Disconnected)), + request_id: AtomicU64::new(1), + child_process: Arc::new(Mutex::new(None)), + stdin_tx: Arc::new(Mutex::new(None)), + pending_requests: Arc::new(RwLock::new(HashMap::new())), + tools: Arc::new(RwLock::new(Vec::new())), + } + } + + pub fn server_id(&self) -> &str { + &self.server_id + } + + pub async fn status(&self) -> ConnectionStatus { + self.status.read().await.clone() + } + + pub async fn get_tools(&self) -> Vec { + self.tools.read().await.clone() + } + + pub async fn connect(&self) -> Result<()> { + { + let mut status = self.status.write().await; + *status = ConnectionStatus::Connecting; + } + + let result = match self.config.transport_type { + McpTransportType::Stdio => self.connect_stdio().await, + McpTransportType::Sse => self.connect_sse().await, + }; + + match &result { + Ok(_) => { + let mut status = self.status.write().await; + *status = ConnectionStatus::Connected; + } + Err(e) => { + let mut status = self.status.write().await; + *status = ConnectionStatus::Error(e.to_string()); + } + } + + result + } + + async fn connect_stdio(&self) -> Result<()> { + let command = self.config.command.as_ref() + .ok_or_else(|| anyhow!("No command specified for stdio transport"))?; + + let mut cmd = Command::new(command); + + if let Some(args) = &self.config.args { + cmd.args(args); + } + + if let Some(env) = &self.config.env { + for (key, value) in env { + cmd.env(key, value); + } + } + + cmd.stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + + let mut child = cmd.spawn() + .map_err(|e| anyhow!("Failed to spawn MCP server process: {}", e))?; + + let stdin = child.stdin.take() + .ok_or_else(|| anyhow!("Failed to get stdin handle"))?; + let stdout = child.stdout.take() + .ok_or_else(|| anyhow!("Failed to get stdout handle"))?; + + // Create channel for sending messages to stdin + let (stdin_tx, mut stdin_rx) = mpsc::channel::(100); + + // Spawn stdin writer task + let mut stdin = stdin; + tokio::spawn(async move { + while let Some(msg) = stdin_rx.recv().await { + if let Err(e) = stdin.write_all(msg.as_bytes()).await { + eprintln!("MCP stdin write error: {}", e); + break; + } + if let Err(e) = stdin.write_all(b"\n").await { + eprintln!("MCP stdin newline write error: {}", e); + break; + } + if let Err(e) = stdin.flush().await { + eprintln!("MCP stdin flush error: {}", e); + break; + } + } + }); + + // Spawn stdout reader task + let pending = Arc::clone(&self.pending_requests); + let status = Arc::clone(&self.status); + tokio::spawn(async move { + let reader = BufReader::new(stdout); + let mut lines = reader.lines(); + + while let Ok(Some(line)) = lines.next_line().await { + if line.trim().is_empty() { + continue; + } + + match serde_json::from_str::(&line) { + Ok(response) => { + if let Some(id) = response.id { + let mut pending_guard = pending.write().await; + if let Some(sender) = pending_guard.remove(&id) { + let result = if let Some(error) = response.error { + Err(anyhow!("JSON-RPC error {}: {}", error.code, error.message)) + } else { + Ok(response.result.unwrap_or(Value::Null)) + }; + let _ = sender.send(result); + } + } + } + Err(e) => { + eprintln!("MCP stdout parse error: {} for line: {}", e, line); + } + } + } + + // Connection closed + let mut status_guard = status.write().await; + *status_guard = ConnectionStatus::Disconnected; + }); + + // Store handles + { + let mut child_guard = self.child_process.lock().await; + *child_guard = Some(child); + } + { + let mut stdin_guard = self.stdin_tx.lock().await; + *stdin_guard = Some(stdin_tx); + } + + // Initialize the connection + self.initialize().await?; + + // List available tools + self.refresh_tools().await?; + + Ok(()) + } + + async fn connect_sse(&self) -> Result<()> { + // SSE transport implementation + // For now, return an error - SSE requires more complex handling + Err(anyhow!("SSE transport not yet implemented")) + } + + async fn send_request(&self, method: &str, params: Option) -> Result { + let id = self.request_id.fetch_add(1, Ordering::SeqCst); + + let request = JsonRpcRequest { + jsonrpc: "2.0".to_string(), + id, + method: method.to_string(), + params, + }; + + let request_json = serde_json::to_string(&request)?; + + // Create response channel + let (tx, rx) = tokio::sync::oneshot::channel(); + + { + let mut pending = self.pending_requests.write().await; + pending.insert(id, tx); + } + + // Send request + { + let stdin_guard = self.stdin_tx.lock().await; + if let Some(stdin) = stdin_guard.as_ref() { + stdin.send(request_json).await + .map_err(|e| anyhow!("Failed to send request: {}", e))?; + } else { + return Err(anyhow!("Not connected")); + } + } + + // Wait for response with timeout + let timeout_ms = self.config.connection_timeout_ms.unwrap_or(30000) as u64; + match timeout(Duration::from_millis(timeout_ms), rx).await { + Ok(Ok(result)) => result, + Ok(Err(_)) => Err(anyhow!("Response channel closed")), + Err(_) => Err(anyhow!("Request timed out")), + } + } + + async fn initialize(&self) -> Result<()> { + let params = InitializeParams { + protocol_version: "2024-11-05".to_string(), + capabilities: ClientCapabilities {}, + client_info: ClientInfo { + name: "ChatMe".to_string(), + version: "0.5.0".to_string(), + }, + }; + + let result = self.send_request("initialize", Some(serde_json::to_value(params)?)).await?; + let _init_result: InitializeResult = serde_json::from_value(result)?; + + // Send initialized notification + let stdin_guard = self.stdin_tx.lock().await; + if let Some(stdin) = stdin_guard.as_ref() { + let notification = json!({ + "jsonrpc": "2.0", + "method": "notifications/initialized" + }); + stdin.send(serde_json::to_string(¬ification)?).await + .map_err(|e| anyhow!("Failed to send initialized notification: {}", e))?; + } + + Ok(()) + } + + pub async fn refresh_tools(&self) -> Result> { + let result = self.send_request("tools/list", None).await?; + let list_result: ListToolsResult = serde_json::from_value(result)?; + + { + let mut tools = self.tools.write().await; + *tools = list_result.tools.clone(); + } + + Ok(list_result.tools) + } + + pub async fn call_tool(&self, name: &str, arguments: Option) -> Result { + let params = CallToolParams { + name: name.to_string(), + arguments, + }; + + let result = self.send_request("tools/call", Some(serde_json::to_value(params)?)).await?; + let call_result: ToolCallResult = serde_json::from_value(result)?; + + Ok(call_result) + } + + pub async fn disconnect(&self) -> Result<()> { + // Close stdin channel + { + let mut stdin_guard = self.stdin_tx.lock().await; + *stdin_guard = None; + } + + // Kill child process if exists + { + let mut child_guard = self.child_process.lock().await; + if let Some(mut child) = child_guard.take() { + let _ = child.kill().await; + } + } + + // Clear pending requests + { + let mut pending = self.pending_requests.write().await; + pending.clear(); + } + + // Clear tools + { + let mut tools = self.tools.write().await; + tools.clear(); + } + + // Update status + { + let mut status = self.status.write().await; + *status = ConnectionStatus::Disconnected; + } + + Ok(()) + } +} + +impl Drop for McpClient { + fn drop(&mut self) { + // Note: async cleanup should be done explicitly via disconnect() + // This is a best-effort cleanup + } +} + +// Manager for all MCP clients +pub struct McpClientManager { + clients: RwLock>>, +} + +impl McpClientManager { + pub fn new() -> Self { + Self { + clients: RwLock::new(HashMap::new()), + } + } + + pub async fn add_server(&self, config: McpServer) -> Arc { + let client = Arc::new(McpClient::new(config.clone())); + let mut clients = self.clients.write().await; + clients.insert(config.id.clone(), Arc::clone(&client)); + client + } + + pub async fn remove_server(&self, server_id: &str) -> Result<()> { + let client = { + let mut clients = self.clients.write().await; + clients.remove(server_id) + }; + + if let Some(client) = client { + client.disconnect().await?; + } + + Ok(()) + } + + pub async fn get_client(&self, server_id: &str) -> Option> { + let clients = self.clients.read().await; + clients.get(server_id).cloned() + } + + pub async fn connect_server(&self, server_id: &str) -> Result<()> { + let client = self.get_client(server_id).await + .ok_or_else(|| anyhow!("Server not found: {}", server_id))?; + client.connect().await + } + + pub async fn disconnect_server(&self, server_id: &str) -> Result<()> { + let client = self.get_client(server_id).await + .ok_or_else(|| anyhow!("Server not found: {}", server_id))?; + client.disconnect().await + } + + pub async fn get_all_tools(&self) -> Vec<(String, McpToolInfo)> { + let clients = self.clients.read().await; + let mut all_tools = Vec::new(); + + for (server_id, client) in clients.iter() { + let tools = client.get_tools().await; + for tool in tools { + all_tools.push((server_id.clone(), tool)); + } + } + + all_tools + } + + pub async fn call_tool(&self, server_id: &str, tool_name: &str, arguments: Option) -> Result { + let client = self.get_client(server_id).await + .ok_or_else(|| anyhow!("Server not found: {}", server_id))?; + + let status = client.status().await; + if status != ConnectionStatus::Connected { + return Err(anyhow!("Server not connected")); + } + + client.call_tool(tool_name, arguments).await + } + + pub async fn get_server_status(&self, server_id: &str) -> Option { + let client = self.get_client(server_id).await?; + Some(client.status().await) + } + + pub async fn get_all_statuses(&self) -> HashMap { + let clients = self.clients.read().await; + let mut statuses = HashMap::new(); + + for (server_id, client) in clients.iter() { + statuses.insert(server_id.clone(), client.status().await); + } + + statuses + } +} + +impl Default for McpClientManager { + fn default() -> Self { + Self::new() + } +} diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 5708d77..f0f126c 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -20,6 +20,139 @@ pub struct Message { pub created_at: DateTime, #[sqlx(skip)] pub images: Option>, + pub permission_request_id: Option, + #[sqlx(skip)] + pub permission_request: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct PermissionRequest { + pub id: String, + pub chat_id: String, + pub operation: String, + pub description: String, + pub level: String, // "Safe" | "Moderate" | "Dangerous" + #[sqlx(skip)] + pub details: std::collections::HashMap, + pub status: String, // "pending" | "approved" | "denied" + pub created_at: DateTime, + pub updated_at: DateTime, +} + +// Tool Execution - persisted tool call history +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct ToolExecutionRecord { + pub id: String, + pub message_id: String, + pub tool_call_id: String, + pub tool_name: String, + pub tool_source: String, // "builtin" or mcp_server_id + #[sqlx(skip)] + pub arguments: serde_json::Value, + #[sqlx(skip)] + pub result: Option, + pub success: bool, + pub error_message: Option, + pub execution_order: i32, + pub started_at: DateTime, + pub completed_at: Option>, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CreateToolExecutionRequest { + pub message_id: String, + pub tool_call_id: String, + pub tool_name: String, + pub tool_source: String, + pub arguments: serde_json::Value, + pub result: Option, + pub success: bool, + pub error_message: Option, + pub execution_order: i32, + pub started_at: String, // ISO string from frontend + pub completed_at: Option, +} + +// MCP Server - configuration for Model Context Protocol servers +#[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type, PartialEq)] +#[sqlx(type_name = "TEXT")] +#[serde(rename_all = "lowercase")] +pub enum McpTransportType { + #[sqlx(rename = "stdio")] + Stdio, + #[sqlx(rename = "sse")] + Sse, +} + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct McpServer { + pub id: String, + pub name: String, + pub transport_type: McpTransportType, + pub command: Option, + #[sqlx(skip)] + pub args: Option>, + #[sqlx(skip)] + pub env: Option>, + pub url: Option, + #[sqlx(skip)] + pub headers: Option>, + pub enabled: bool, + pub auto_connect: bool, + pub connection_timeout_ms: Option, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CreateMcpServerRequest { + pub name: String, + pub transport_type: McpTransportType, + pub command: Option, + pub args: Option>, + pub env: Option>, + pub url: Option, + pub headers: Option>, + pub enabled: bool, + pub auto_connect: bool, + pub connection_timeout_ms: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UpdateMcpServerRequest { + pub name: String, + pub transport_type: McpTransportType, + pub command: Option, + pub args: Option>, + pub env: Option>, + pub url: Option, + pub headers: Option>, + pub enabled: bool, + pub auto_connect: bool, + pub connection_timeout_ms: Option, +} + +// MCP Tool - tool discovered from an MCP server +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct McpTool { + pub id: String, + pub server_id: String, + pub name: String, + pub description: Option, + #[sqlx(skip)] + pub input_schema: serde_json::Value, + pub enabled: bool, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +// MCP Server with its tools for API responses +#[derive(Debug, Serialize, Deserialize)] +pub struct McpServerWithTools { + #[serde(flatten)] + pub server: McpServer, + pub tools: Vec, + pub connection_status: String, // "connected" | "disconnected" | "error" } #[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)] @@ -30,6 +163,8 @@ pub enum MessageRole { User, #[sqlx(rename = "assistant")] Assistant, + #[sqlx(rename = "system")] + System, } #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] @@ -47,7 +182,7 @@ pub struct ApiConfig { pub updated_at: DateTime, } -#[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type)] +#[derive(Debug, Clone, Serialize, Deserialize, sqlx::Type, PartialEq)] #[sqlx(type_name = "TEXT")] #[serde(rename_all = "lowercase")] pub enum ApiProvider { @@ -59,6 +194,22 @@ pub enum ApiProvider { Google, #[sqlx(rename = "ollama")] Ollama, + #[sqlx(rename = "mistral")] + Mistral, + #[sqlx(rename = "deepseek")] + DeepSeek, + #[sqlx(rename = "lmstudio")] + LMStudio, + #[sqlx(rename = "kimi")] + Kimi, + #[sqlx(rename = "openrouter")] + OpenRouter, + #[sqlx(rename = "together")] + Together, + #[sqlx(rename = "groq")] + Groq, + #[sqlx(rename = "perplexity")] + Perplexity, #[sqlx(rename = "custom")] Custom, } @@ -88,6 +239,7 @@ pub struct CreateMessageRequest { pub content: String, pub role: MessageRole, pub images: Option>, + pub permission_request_id: Option, } #[derive(Debug, Serialize, Deserialize)] @@ -119,26 +271,22 @@ pub struct UpdateApiConfigRequest { pub is_default: bool, } -#[derive(Debug, Serialize, Deserialize)] -pub struct ChatCompletionRequest { - pub messages: Vec, - pub model: String, - pub temperature: f32, - pub max_tokens: Option, -} -#[derive(Debug, Serialize, Deserialize)] -pub struct ChatMessage { - pub role: String, - pub content: serde_json::Value, // Can be string or array of content objects + + + +// Tool Definition Types (OpenAI format as canonical) +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct ToolDefinition { + #[serde(rename = "type")] + pub tool_type: String, // Always "function" + pub function: FunctionDefinition, } -#[derive(Debug, Serialize, Deserialize)] -pub struct ChatCompletionResponse { - pub choices: Vec, +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct FunctionDefinition { + pub name: String, + pub description: String, + pub parameters: serde_json::Value, // JSON Schema object } -#[derive(Debug, Serialize, Deserialize)] -pub struct ChatChoice { - pub message: ChatMessage, -} \ No newline at end of file diff --git a/src-tauri/src/permission_manager.rs b/src-tauri/src/permission_manager.rs new file mode 100644 index 0000000..c79d17e --- /dev/null +++ b/src-tauri/src/permission_manager.rs @@ -0,0 +1,241 @@ +use serde::{Deserialize, Serialize}; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use tokio::sync::{Mutex, oneshot}; +use uuid::Uuid; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub enum PermissionLevel { + Safe, + Moderate, + Dangerous, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct PermissionRequest { + pub id: String, + pub operation: String, + pub description: String, + pub level: PermissionLevel, + pub details: HashMap, + pub chat_id: Option, +} + +pub struct PendingPermission { + pub sender: oneshot::Sender, +} + +#[derive(Clone)] +pub struct PermissionManager { + pending: Arc>>, + // Cache of approved permissions per chat: chat_id -> Set + approved_cache: Arc>>>, + // Track currently pending operations: (chat_id, operation) -> permission_id + pending_operations: Arc>>, +} + +impl PermissionManager { + pub fn new() -> Self { + Self { + pending: Arc::new(Mutex::new(HashMap::new())), + approved_cache: Arc::new(Mutex::new(HashMap::new())), + pending_operations: Arc::new(Mutex::new(HashMap::new())), + } + } + + pub async fn request_permission(&self, request: PermissionRequest) -> Result { + println!("[RUST PermMgr] request_permission called for ID: {}, operation: {}, level: {:?}", + request.id, request.operation, request.level); + + // If it's a safe operation, auto-approve + if request.level == PermissionLevel::Safe { + println!("[RUST PermMgr] Auto-approving Safe operation"); + return Ok(true); + } + + // Check if this permission was already granted for this chat + if let Some(ref chat_id) = request.chat_id { + let cache = self.approved_cache.lock().await; + if let Some(approved_ops) = cache.get(chat_id) { + if approved_ops.contains(&request.operation) { + println!("[RUST PermMgr] Permission already granted for this chat, auto-approving"); + return Ok(true); + } + } + } + + let id = request.id.clone(); + let operation = request.operation.clone(); + let chat_id = request.chat_id.clone(); + let (tx, rx) = oneshot::channel(); + + println!("[RUST PermMgr] Creating oneshot channel and storing pending request"); + + // Store the pending request + { + let mut pending = self.pending.lock().await; + pending.insert(id.clone(), PendingPermission { + sender: tx, + }); + println!("[RUST PermMgr] Pending request stored, total pending: {}", pending.len()); + } + + // Track this operation as pending + if let Some(ref chat_id) = chat_id { + let mut pending_ops = self.pending_operations.lock().await; + let key = (chat_id.clone(), operation.clone()); + pending_ops.insert(key, id.clone()); + println!("[RUST PermMgr] Tracking pending operation: {} for chat: {}", operation, chat_id); + } + + println!("[RUST PermMgr] Waiting for user response via channel..."); + + // Wait for response with timeout + match tokio::time::timeout(std::time::Duration::from_secs(300), rx).await { + Ok(Ok(approved)) => { + println!("[RUST PermMgr] Received response via channel: {}", approved); + + // If approved and chat_id exists, cache the permission + if approved { + if let Some(ref chat_id) = chat_id { + let mut cache = self.approved_cache.lock().await; + cache.entry(chat_id.clone()) + .or_insert_with(HashSet::new) + .insert(operation.clone()); + println!("[RUST PermMgr] Permission cached for future use"); + } + } + + // Clean up pending request + let mut pending = self.pending.lock().await; + pending.remove(&id); + + // Clean up pending operation tracking + if let Some(ref chat_id) = chat_id { + let mut pending_ops = self.pending_operations.lock().await; + let key = (chat_id.clone(), operation.clone()); + pending_ops.remove(&key); + println!("[RUST PermMgr] Removed pending operation tracking"); + } + + Ok(approved) + } + Ok(Err(_)) => { + println!("[RUST PermMgr] Channel closed without response"); + // Clean up pending request + let mut pending = self.pending.lock().await; + pending.remove(&id); + + // Clean up pending operation tracking + if let Some(ref chat_id) = chat_id { + let mut pending_ops = self.pending_operations.lock().await; + let key = (chat_id.clone(), operation.clone()); + pending_ops.remove(&key); + } + + Err("Permission request cancelled".to_string()) + } + Err(_) => { + println!("[RUST PermMgr] Timeout waiting for response"); + // Clean up pending request + let mut pending = self.pending.lock().await; + pending.remove(&id); + + // Clean up pending operation tracking + if let Some(ref chat_id) = chat_id { + let mut pending_ops = self.pending_operations.lock().await; + let key = (chat_id.clone(), operation.clone()); + pending_ops.remove(&key); + } + + Err("Permission request timed out".to_string()) + } + } + } + + pub async fn respond_to_permission(&self, request_id: String, approved: bool) -> Result<(), String> { + let mut pending = self.pending.lock().await; + + if let Some(pending_request) = pending.remove(&request_id) { + pending_request.sender.send(approved) + .map_err(|_| "Failed to send permission response".to_string())?; + Ok(()) + } else { + Err("Permission request not found".to_string()) + } + } + + /// Clear all cached permissions for a specific chat + pub async fn clear_chat_permissions(&self, chat_id: String) -> Result<(), String> { + let mut cache = self.approved_cache.lock().await; + cache.remove(&chat_id); + println!("[RUST PermMgr] Cleared all permissions for chat: {}", chat_id); + Ok(()) + } + + /// Clear a specific permission for a chat + pub async fn clear_permission(&self, chat_id: String, operation: String) -> Result<(), String> { + let mut cache = self.approved_cache.lock().await; + if let Some(permissions) = cache.get_mut(&chat_id) { + permissions.remove(&operation); + println!("[RUST PermMgr] Cleared permission '{}' for chat: {}", operation, chat_id); + } + Ok(()) + } + + /// Get all cached permissions for a chat + pub async fn get_chat_permissions(&self, chat_id: String) -> Vec { + let cache = self.approved_cache.lock().await; + cache.get(&chat_id) + .map(|set| set.iter().cloned().collect()) + .unwrap_or_default() + } + + /// Check if a permission is cached for a chat + pub async fn is_permission_cached(&self, chat_id: Option, operation: String) -> bool { + if let Some(ref chat_id_str) = chat_id { + let cache = self.approved_cache.lock().await; + println!("[RUST PermMgr] Checking cache for chat_id: {}, operation: {}", chat_id_str, operation); + + if let Some(approved_ops) = cache.get(chat_id_str) { + let ops_vec: Vec<&String> = approved_ops.iter().collect(); + println!("[RUST PermMgr] Found {} cached operations for this chat: {:?}", ops_vec.len(), ops_vec); + let result = approved_ops.contains(&operation); + println!("[RUST PermMgr] Operation '{}' {} in cache", operation, if result { "FOUND" } else { "NOT FOUND" }); + return result; + } else { + println!("[RUST PermMgr] No cached operations found for chat_id: {}", chat_id_str); + } + } else { + println!("[RUST PermMgr] chat_id is None, cannot check cache"); + } + false + } + + /// Check if there's already a pending permission request for this operation in this chat + pub async fn has_pending_permission(&self, chat_id: Option, operation: &str) -> bool { + if let Some(chat_id) = chat_id { + let pending_ops = self.pending_operations.lock().await; + let key = (chat_id, operation.to_string()); + return pending_ops.contains_key(&key); + } + false + } +} + +pub fn create_permission_request( + operation: &str, + description: String, + level: PermissionLevel, + details: HashMap, + chat_id: Option, +) -> PermissionRequest { + PermissionRequest { + id: Uuid::new_v4().to_string(), + operation: operation.to_string(), + description, + level, + details, + chat_id, + } +} diff --git a/src-tauri/src/system_operations.rs b/src-tauri/src/system_operations.rs index 26ea564..50a784b 100644 --- a/src-tauri/src/system_operations.rs +++ b/src-tauri/src/system_operations.rs @@ -65,41 +65,122 @@ pub struct OperationPermission { } // App launching functions -pub fn launch_application(app_path: &str, args: Option>) -> Result { - let path = Path::new(app_path); - - if !path.exists() { - return Err(anyhow!("Application path does not exist: {}", app_path)); - } +pub fn launch_application(app_name_or_path: &str, args: Option>) -> Result { + let input = app_name_or_path.trim(); + + // Check if it's a URL (http://, https://, or custom protocol like spotify:) + let is_url = input.starts_with("http://") + || input.starts_with("https://") + || (input.contains(":") && !input.contains(":\\") && !input.contains(":/")); + + // Check if it looks like a full path + let is_full_path = Path::new(input).is_absolute() || input.contains("\\") || input.starts_with("/"); let mut command = if cfg!(target_os = "windows") { - let mut cmd = Command::new("cmd"); - cmd.args(&["/C", "start", "", app_path]); - if let Some(arguments) = args { - for arg in arguments { - cmd.arg(arg); + if is_url { + // Open URL with default browser + let mut cmd = Command::new("cmd"); + cmd.args(&["/C", "start", "", input]); + cmd + } else if is_full_path { + // It's a full path - check if exists + let path = Path::new(input); + if !path.exists() { + return Err(anyhow!("Application path does not exist: {}", input)); + } + let mut cmd = Command::new("cmd"); + cmd.args(&["/C", "start", "", input]); + if let Some(arguments) = &args { + for arg in arguments { + cmd.arg(arg); + } + } + cmd + } else { + // It's an app name - try to launch it + // Windows can resolve common app names through PATH, Start Menu, etc. + let resolved_app = resolve_windows_app_name(input); + + let mut cmd = Command::new("cmd"); + cmd.args(&["/C", "start", "", &resolved_app]); + if let Some(arguments) = &args { + for arg in arguments { + cmd.arg(arg); + } } + cmd } - cmd } else if cfg!(target_os = "macos") { - let mut cmd = Command::new("open"); - cmd.arg(app_path); - if let Some(arguments) = args { - cmd.arg("--args"); - for arg in arguments { - cmd.arg(arg); + if is_url { + // Open URL with default browser + let mut cmd = Command::new("open"); + cmd.arg(input); + cmd + } else if is_full_path { + let path = Path::new(input); + if !path.exists() { + return Err(anyhow!("Application path does not exist: {}", input)); } + let mut cmd = Command::new("open"); + cmd.arg(input); + if let Some(arguments) = &args { + cmd.arg("--args"); + for arg in arguments { + cmd.arg(arg); + } + } + cmd + } else { + // Try to find the app by name + let resolved_app = resolve_macos_app_name(input); + let mut cmd = Command::new("open"); + + // Check if it's an app bundle path or just an app name + if resolved_app.ends_with(".app") && Path::new(&resolved_app).exists() { + cmd.arg(&resolved_app); + } else { + // Use -a flag to open by application name + cmd.args(&["-a", &resolved_app]); + } + + if let Some(arguments) = &args { + cmd.arg("--args"); + for arg in arguments { + cmd.arg(arg); + } + } + cmd } - cmd } else { // Linux - let mut cmd = Command::new(app_path); - if let Some(arguments) = args { - for arg in arguments { - cmd.arg(arg); + if is_url { + // Open URL with xdg-open + let mut cmd = Command::new("xdg-open"); + cmd.arg(input); + cmd + } else if is_full_path { + let path = Path::new(input); + if !path.exists() { + return Err(anyhow!("Application path does not exist: {}", input)); + } + let mut cmd = Command::new(input); + if let Some(arguments) = &args { + for arg in arguments { + cmd.arg(arg); + } } + cmd + } else { + // Try to find the app in PATH or common locations + let resolved_app = resolve_linux_app_name(input); + let mut cmd = Command::new(&resolved_app); + if let Some(arguments) = &args { + for arg in arguments { + cmd.arg(arg); + } + } + cmd } - cmd }; let child = command @@ -111,6 +192,181 @@ pub fn launch_application(app_path: &str, args: Option>) -> Result String { + let name_lower = name.to_lowercase(); + + // Map common app names to their executable names + match name_lower.as_str() { + // Browsers + "chrome" | "google chrome" => "chrome".to_string(), + "firefox" | "mozilla firefox" => "firefox".to_string(), + "edge" | "microsoft edge" => "msedge".to_string(), + "brave" => "brave".to_string(), + "opera" => "opera".to_string(), + + // Microsoft Office + "word" | "microsoft word" => "winword".to_string(), + "excel" | "microsoft excel" => "excel".to_string(), + "powerpoint" | "microsoft powerpoint" => "powerpnt".to_string(), + "outlook" | "microsoft outlook" => "outlook".to_string(), + "onenote" => "onenote".to_string(), + + // Development + "vscode" | "visual studio code" | "code" => "code".to_string(), + "visual studio" => "devenv".to_string(), + "notepad++" | "notepadplusplus" => "notepad++".to_string(), + "sublime" | "sublime text" => "subl".to_string(), + + // System + "notepad" => "notepad".to_string(), + "calculator" | "calc" => "calc".to_string(), + "paint" => "mspaint".to_string(), + "explorer" | "file explorer" => "explorer".to_string(), + "cmd" | "command prompt" => "cmd".to_string(), + "powershell" => "powershell".to_string(), + "terminal" | "windows terminal" => "wt".to_string(), + "task manager" | "taskmgr" => "taskmgr".to_string(), + "control panel" => "control".to_string(), + "settings" => "ms-settings:".to_string(), + + // Media + "spotify" => "spotify".to_string(), + "vlc" => "vlc".to_string(), + "itunes" => "itunes".to_string(), + + // Communication + "discord" => "discord".to_string(), + "slack" => "slack".to_string(), + "teams" | "microsoft teams" => "teams".to_string(), + "zoom" => "zoom".to_string(), + "skype" => "skype".to_string(), + + // Other + "steam" => "steam".to_string(), + "epic" | "epic games" => "EpicGamesLauncher".to_string(), + + // If not found, return as-is (let Windows try to resolve it) + _ => name.to_string(), + } +} + +/// Resolve common macOS application names +fn resolve_macos_app_name(name: &str) -> String { + let name_lower = name.to_lowercase(); + + match name_lower.as_str() { + // Browsers + "chrome" | "google chrome" => "Google Chrome".to_string(), + "firefox" | "mozilla firefox" => "Firefox".to_string(), + "safari" => "Safari".to_string(), + "brave" => "Brave Browser".to_string(), + "edge" | "microsoft edge" => "Microsoft Edge".to_string(), + + // Development + "vscode" | "visual studio code" | "code" => "Visual Studio Code".to_string(), + "xcode" => "Xcode".to_string(), + "terminal" => "Terminal".to_string(), + "iterm" | "iterm2" => "iTerm".to_string(), + "sublime" | "sublime text" => "Sublime Text".to_string(), + + // Productivity + "finder" => "Finder".to_string(), + "notes" => "Notes".to_string(), + "calendar" => "Calendar".to_string(), + "mail" => "Mail".to_string(), + "messages" => "Messages".to_string(), + "facetime" => "FaceTime".to_string(), + "preview" => "Preview".to_string(), + "textedit" | "text edit" => "TextEdit".to_string(), + + // Media + "spotify" => "Spotify".to_string(), + "music" | "apple music" => "Music".to_string(), + "vlc" => "VLC".to_string(), + "photos" => "Photos".to_string(), + + // Microsoft Office + "word" | "microsoft word" => "Microsoft Word".to_string(), + "excel" | "microsoft excel" => "Microsoft Excel".to_string(), + "powerpoint" | "microsoft powerpoint" => "Microsoft PowerPoint".to_string(), + "outlook" | "microsoft outlook" => "Microsoft Outlook".to_string(), + "teams" | "microsoft teams" => "Microsoft Teams".to_string(), + + // Communication + "slack" => "Slack".to_string(), + "discord" => "Discord".to_string(), + "zoom" => "zoom.us".to_string(), + "skype" => "Skype".to_string(), + + // Other + "activity monitor" => "Activity Monitor".to_string(), + "system preferences" | "settings" => "System Preferences".to_string(), + "app store" => "App Store".to_string(), + + // If not found, capitalize first letter of each word + _ => name.split_whitespace() + .map(|word| { + let mut chars = word.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().collect::() + chars.as_str(), + } + }) + .collect::>() + .join(" "), + } +} + +/// Resolve common Linux application names +fn resolve_linux_app_name(name: &str) -> String { + let name_lower = name.to_lowercase(); + + match name_lower.as_str() { + // Browsers + "chrome" | "google chrome" => "google-chrome".to_string(), + "chromium" => "chromium-browser".to_string(), + "firefox" | "mozilla firefox" => "firefox".to_string(), + "brave" => "brave-browser".to_string(), + "edge" | "microsoft edge" => "microsoft-edge".to_string(), + + // Development + "vscode" | "visual studio code" | "code" => "code".to_string(), + "sublime" | "sublime text" => "subl".to_string(), + "atom" => "atom".to_string(), + "gedit" => "gedit".to_string(), + + // File managers + "nautilus" | "files" => "nautilus".to_string(), + "dolphin" => "dolphin".to_string(), + "thunar" => "thunar".to_string(), + + // Terminals + "terminal" => "gnome-terminal".to_string(), + "konsole" => "konsole".to_string(), + "xterm" => "xterm".to_string(), + + // Media + "spotify" => "spotify".to_string(), + "vlc" => "vlc".to_string(), + + // Communication + "slack" => "slack".to_string(), + "discord" => "discord".to_string(), + "teams" | "microsoft teams" => "teams".to_string(), + "zoom" => "zoom".to_string(), + + // Calculator + "calculator" | "calc" => "gnome-calculator".to_string(), + + // Settings + "settings" => "gnome-control-center".to_string(), + + // If not found, return as-is (assume it's in PATH) + _ => name.to_string(), + } +} + // Get list of installed applications pub fn get_installed_applications() -> Result> { let mut apps = Vec::new(); @@ -512,11 +768,87 @@ pub fn check_permission_level(operation: &str, params: &HashMap { + if let Some(app_name) = params.get("app_name").and_then(|v| v.as_str()) { + details.insert("application".to_string(), app_name.to_string()); + OperationPermission { + operation: "Launch Application".to_string(), + description: format!("Launch application: {}", app_name), + level: PermissionLevel::Moderate, + details, + } + } else { + OperationPermission { + operation: "Launch Application".to_string(), + description: "Launch unknown application".to_string(), + level: PermissionLevel::Moderate, + details, + } + } + }, + + "write_file" => { + if let Some(path) = params.get("file_path").and_then(|v| v.as_str()) { + details.insert("path".to_string(), path.to_string()); + + // Check if it's a system file + let system_dirs = vec![ + "C:\\Windows", "C:\\Program Files", "/usr", "/bin", "/etc", + "/System", "/Library", "/Applications" + ]; + + let is_system = system_dirs.iter() + .any(|dir| path.starts_with(dir)); + + OperationPermission { + operation: "Write File".to_string(), + description: format!("Write to file: {}", path), + level: if is_system { + PermissionLevel::Dangerous + } else { + PermissionLevel::Moderate + }, + details, + } + } else { + OperationPermission { + operation: "Write File".to_string(), + description: "Write to unknown file".to_string(), + level: PermissionLevel::Moderate, + details, + } + } + }, + + "read_file" | "search_files" | "read_directory" | "list_directory" | "open_file" => { + if let Some(path) = params.get("file_path") + .or_else(|| params.get("directory_path")) + .and_then(|v| v.as_str()) + { + details.insert("path".to_string(), path.to_string()); + } + OperationPermission { + operation: operation.to_string(), + description: format!("Read operation: {}", operation), + level: PermissionLevel::Safe, + details, + } + }, + + "get_current_directory" | "list_processes" => { + OperationPermission { + operation: operation.to_string(), + description: format!("Information query: {}", operation), + level: PermissionLevel::Safe, + details, + } + }, + _ => OperationPermission { operation: operation.to_string(), - description: "Unknown operation".to_string(), - level: PermissionLevel::Safe, + description: format!("Operation: {}", operation), + level: PermissionLevel::Moderate, // Changed from Safe to Moderate! details, } } diff --git a/src-tauri/src/tool_registry.rs b/src-tauri/src/tool_registry.rs new file mode 100644 index 0000000..2aae758 --- /dev/null +++ b/src-tauri/src/tool_registry.rs @@ -0,0 +1,271 @@ +use anyhow::{anyhow, Result}; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; + +use crate::agentic::AgentSession; +use crate::tools::get_all_tool_definitions; +use crate::mcp_client::{McpClientManager, McpToolInfo, ToolCallResult, ToolContent}; +use crate::models::ToolDefinition; + +/// Identifies where a tool comes from +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub enum ToolSource { + Builtin, + McpServer(String), // server_id +} + +impl ToolSource { + pub fn as_str(&self) -> String { + match self { + ToolSource::Builtin => "builtin".to_string(), + ToolSource::McpServer(id) => id.clone(), + } + } +} + +/// A tool definition with its source +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolWithSource { + pub source: ToolSource, + pub definition: ToolDefinition, +} + +/// Registry that manages all available tools from all sources +pub struct ToolRegistry { + mcp_manager: Arc, + // Cache of MCP tools converted to ToolDefinition format + mcp_tools_cache: RwLock>>, +} + +impl ToolRegistry { + pub fn new(mcp_manager: Arc) -> Self { + Self { + mcp_manager, + mcp_tools_cache: RwLock::new(HashMap::new()), + } + } + + /// Get all builtin tool definitions + pub fn get_builtin_tools(&self) -> Vec { + get_all_tool_definitions() + .into_iter() + .map(|def| ToolWithSource { + source: ToolSource::Builtin, + definition: def, + }) + .collect() + } + + /// Convert MCP tool info to our ToolDefinition format + fn convert_mcp_tool(server_id: &str, tool: &McpToolInfo) -> ToolWithSource { + // Prefix the tool name with server ID to avoid conflicts + let prefixed_name = format!("mcp_{}_{}", server_id.replace("-", "_"), tool.name); + + ToolWithSource { + source: ToolSource::McpServer(server_id.to_string()), + definition: ToolDefinition { + tool_type: "function".to_string(), + function: crate::models::FunctionDefinition { + name: prefixed_name, + description: tool.description.clone().unwrap_or_else(|| format!("MCP tool: {}", tool.name)), + parameters: tool.input_schema.clone(), + }, + }, + } + } + + /// Refresh the cache of MCP tools from all connected servers + pub async fn refresh_mcp_tools(&self) -> Result<()> { + let all_mcp_tools = self.mcp_manager.get_all_tools().await; + + let mut cache = self.mcp_tools_cache.write().await; + cache.clear(); + + for (server_id, tool_info) in all_mcp_tools { + let tool_with_source = Self::convert_mcp_tool(&server_id, &tool_info); + cache.entry(server_id) + .or_insert_with(Vec::new) + .push(tool_with_source); + } + + Ok(()) + } + + /// Get all MCP tools from the cache + pub async fn get_mcp_tools(&self) -> Vec { + let cache = self.mcp_tools_cache.read().await; + cache.values().flatten().cloned().collect() + } + + /// Get all tools (builtin + MCP) + pub async fn get_all_tools(&self) -> Vec { + let mut all_tools = self.get_builtin_tools(); + all_tools.extend(self.get_mcp_tools().await); + all_tools + } + + /// Get just the tool definitions (for sending to LLM) + pub async fn get_all_tool_definitions(&self) -> Vec { + self.get_all_tools() + .await + .into_iter() + .map(|t| t.definition) + .collect() + } + + /// Find which source a tool comes from by its name + pub async fn find_tool_source(&self, tool_name: &str) -> Option { + // Check builtin first + let builtin = self.get_builtin_tools(); + if builtin.iter().any(|t| t.definition.function.name == tool_name) { + return Some(ToolSource::Builtin); + } + + // Check MCP tools + let mcp_tools = self.get_mcp_tools().await; + for tool in mcp_tools { + if tool.definition.function.name == tool_name { + return Some(tool.source); + } + } + + None + } + + /// Extract the original MCP tool name from the prefixed name + pub fn extract_mcp_tool_name(prefixed_name: &str) -> Option<(String, String)> { + // Format: mcp_{server_id}_{tool_name} + if !prefixed_name.starts_with("mcp_") { + return None; + } + + let without_prefix = &prefixed_name[4..]; // Remove "mcp_" + + // Find the first underscore which separates server_id from tool_name + // This is tricky because server_id also has underscores (was dashes) + // We need to find a way to separate them + // For now, we'll store a mapping when we create the tool + + // Actually, let's use a different approach - store the mapping + None // This will be handled by looking up in our cache + } + + /// Get original tool name and server ID from a prefixed MCP tool name + pub async fn get_mcp_tool_info(&self, prefixed_name: &str) -> Option<(String, String)> { + let cache = self.mcp_tools_cache.read().await; + + for (server_id, tools) in cache.iter() { + for tool in tools { + if tool.definition.function.name == prefixed_name { + // Extract original name by removing prefix + let prefix = format!("mcp_{}_", server_id.replace("-", "_")); + if let Some(original_name) = prefixed_name.strip_prefix(&prefix) { + return Some((server_id.clone(), original_name.to_string())); + } + } + } + } + + None + } + + /// Execute a tool call, routing to the appropriate backend + pub async fn execute_tool( + &self, + session: &mut AgentSession, + tool_name: &str, + arguments: Value, + ) -> Result<(ToolSource, Value)> { + // Determine tool source + let source = self.find_tool_source(tool_name).await + .ok_or_else(|| anyhow!("Unknown tool: {}", tool_name))?; + + match &source { + ToolSource::Builtin => { + // Convert Value to HashMap + let params: HashMap = match arguments { + Value::Object(map) => map.into_iter().collect(), + _ => HashMap::new(), + }; + + // Execute using AgentSession + let action = session.execute_action(tool_name, params).await?; + + if action.success { + Ok((source, action.result.unwrap_or(Value::Null))) + } else { + Err(anyhow!(action.error_message.unwrap_or_else(|| "Unknown error".to_string()))) + } + } + ToolSource::McpServer(server_id) => { + // Get original tool name + let (_, original_name) = self.get_mcp_tool_info(tool_name).await + .ok_or_else(|| anyhow!("Could not find MCP tool info for: {}", tool_name))?; + + // Execute via MCP client + let result = self.mcp_manager + .call_tool(server_id, &original_name, Some(arguments)) + .await?; + + // Convert MCP result to our format + let result_value = self.convert_mcp_result(result)?; + + Ok((source, result_value)) + } + } + } + + /// Convert MCP tool result to a JSON value + fn convert_mcp_result(&self, result: ToolCallResult) -> Result { + if result.is_error { + let error_text = result.content.iter() + .filter_map(|c| match c { + ToolContent::Text { text } => Some(text.clone()), + _ => None, + }) + .collect::>() + .join("\n"); + + return Err(anyhow!("MCP tool error: {}", error_text)); + } + + // Combine all content into a result + let contents: Vec = result.content.iter() + .map(|c| match c { + ToolContent::Text { text } => { + serde_json::json!({ + "type": "text", + "text": text + }) + } + ToolContent::Image { data, mime_type } => { + serde_json::json!({ + "type": "image", + "data": data, + "mimeType": mime_type + }) + } + ToolContent::Resource { resource } => { + serde_json::json!({ + "type": "resource", + "uri": resource.uri, + "mimeType": resource.mime_type, + "text": resource.text + }) + } + }) + .collect(); + + // If there's only one text content, return just the text + if contents.len() == 1 { + if let Some(text) = contents[0].get("text") { + return Ok(text.clone()); + } + } + + Ok(Value::Array(contents)) + } +} diff --git a/src-tauri/src/tools.rs b/src-tauri/src/tools.rs new file mode 100644 index 0000000..9fc617f --- /dev/null +++ b/src-tauri/src/tools.rs @@ -0,0 +1,121 @@ +use crate::models::{ToolDefinition, FunctionDefinition}; +use crate::agentic::{AgentCapability, AgentParameter, AgentSession}; +use serde_json::json; + +/// Convert AgentCapability to OpenAI ToolDefinition format +pub fn capability_to_tool_definition(capability: &AgentCapability) -> ToolDefinition { + ToolDefinition { + tool_type: "function".to_string(), + function: FunctionDefinition { + name: capability.name.clone(), + description: capability.description.clone(), + parameters: build_json_schema(&capability.parameters), + }, + } +} + +/// Build JSON Schema from AgentParameters +fn build_json_schema(parameters: &[AgentParameter]) -> serde_json::Value { + let mut properties = serde_json::Map::new(); + let mut required = Vec::new(); + + for param in parameters { + if param.required { + required.push(param.name.clone()); + } + + let param_type = match param.parameter_type.as_str() { + "string" => "string", + "number" => "number", + "boolean" => "boolean", + "array" => "array", + "object" => "object", + _ => "string", // Default fallback + }; + + let mut param_schema = json!({ + "type": param_type, + "description": param.description, + }); + + if let Some(default) = ¶m.default_value { + param_schema["default"] = default.clone(); + } + + properties.insert(param.name.clone(), param_schema); + } + + json!({ + "type": "object", + "properties": properties, + "required": required, + }) +} + +/// Get all available tools as ToolDefinitions +pub fn get_all_tool_definitions() -> Vec { + let capabilities = AgentSession::get_capabilities(); + capabilities.iter() + .map(capability_to_tool_definition) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_capability_to_tool_definition() { + let cap = AgentCapability { + name: "read_file".to_string(), + description: "Read file contents".to_string(), + parameters: vec![ + AgentParameter { + name: "path".to_string(), + parameter_type: "string".to_string(), + description: "File path".to_string(), + required: true, + default_value: None, + }, + ], + }; + + let tool = capability_to_tool_definition(&cap); + + assert_eq!(tool.function.name, "read_file"); + assert_eq!(tool.tool_type, "function"); + + let params = tool.function.parameters; + assert_eq!(params["type"], "object"); + assert!(params["properties"]["path"].is_object()); + assert_eq!(params["required"], json!(["path"])); + } + + #[test] + fn test_build_json_schema() { + let parameters = vec![ + AgentParameter { + name: "path".to_string(), + parameter_type: "string".to_string(), + description: "File path".to_string(), + required: true, + default_value: None, + }, + AgentParameter { + name: "recursive".to_string(), + parameter_type: "boolean".to_string(), + description: "Recursive flag".to_string(), + required: false, + default_value: Some(json!(false)), + }, + ]; + + let schema = build_json_schema(¶meters); + + assert_eq!(schema["type"], "object"); + assert_eq!(schema["required"], json!(["path"])); + assert_eq!(schema["properties"]["path"]["type"], "string"); + assert_eq!(schema["properties"]["recursive"]["type"], "boolean"); + assert_eq!(schema["properties"]["recursive"]["default"], false); + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index a269a30..f94e0a9 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "ChatMe", - "version": "0.4.0", + "version": "0.5.3", "identifier": "com.amanpreet.chatme", "build": { "beforeDevCommand": "npm run dev", @@ -13,8 +13,8 @@ "windows": [ { "title": "ChatMe", - "width": 1000, - "height": 600, + "width": 1200, + "height": 800, "decorations": false, "titleBarStyle": "Transparent", "resizable": true, diff --git a/src/App.css b/src/App.css index 435b48e..b13f7d5 100644 --- a/src/App.css +++ b/src/App.css @@ -5,37 +5,43 @@ .drag { -webkit-app-region: drag; } + .no-drag { -webkit-app-region: no-drag; } /* Ensure full height layout */ -html, body, #root { +html, +body, +#root { height: 100%; margin: 0; padding: 0; } +/* Smooth theme transitions */ +* { + transition: background-color 0.2s ease, border-color 0.2s ease, color 0.15s ease; +} + /* Mobile touch improvements */ @media (hover: none) and (pointer: coarse) { - /* Touch-friendly button sizing */ - button, [role="button"] { + + button, + [role="button"] { min-height: 44px; min-width: 44px; } - - /* Larger tap targets for mobile */ + .sidebar-trigger { min-height: 48px; min-width: 48px; } - - /* Prevent overscroll on mobile */ + body { overscroll-behavior: none; } - - /* Improve touch scrolling */ + * { -webkit-overflow-scrolling: touch; } @@ -48,18 +54,49 @@ html, body, #root { } } +/* Modern scrollbars */ +::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: hsl(var(--muted-foreground) / 0.3); + border-radius: 3px; +} + +::-webkit-scrollbar-thumb:hover { + background: hsl(var(--muted-foreground) / 0.5); +} + /* Custom markdown styles for assistant messages */ .markdown-content { - line-height: 1.6; + line-height: 1.7; } .markdown-content pre { - background-color: hsl(var(--muted)) !important; - border: 1px solid hsl(var(--border)); - border-radius: 0.5rem; - padding: 1rem; + background: linear-gradient(135deg, hsl(var(--muted) / 0.8), hsl(var(--muted) / 0.6)) !important; + border: 1px solid hsl(var(--border) / 0.6); + border-radius: 0.75rem; + padding: 1.25rem; overflow-x: auto; - margin: 0.5rem 0; + margin: 1rem 0; + position: relative; +} + +.markdown-content pre::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, hsl(var(--primary) / 0.5), hsl(var(--accent) / 0.5)); + border-radius: 0.75rem 0.75rem 0 0; } .markdown-content code { @@ -74,162 +111,239 @@ html, body, #root { } .markdown-content p { - margin-bottom: 0.75rem; + margin-bottom: 0.875rem; } .markdown-content p:last-child { margin-bottom: 0; } -.markdown-content ul, .markdown-content ol { - margin-bottom: 0.75rem; +.markdown-content ul, +.markdown-content ol { + margin-bottom: 0.875rem; padding-left: 1.5rem; } .markdown-content li { - margin-bottom: 0.25rem; + margin-bottom: 0.375rem; +} + +.markdown-content li::marker { + color: hsl(var(--primary)); } -.markdown-content h1, .markdown-content h2, .markdown-content h3, -.markdown-content h4, .markdown-content h5, .markdown-content h6 { +.markdown-content h1, +.markdown-content h2, +.markdown-content h3, +.markdown-content h4, +.markdown-content h5, +.markdown-content h6 { font-weight: 600; - margin-bottom: 0.5rem; - margin-top: 1rem; + margin-bottom: 0.75rem; + margin-top: 1.25rem; + color: hsl(var(--foreground)); } -.markdown-content h1:first-child, .markdown-content h2:first-child, +.markdown-content h1:first-child, +.markdown-content h2:first-child, .markdown-content h3:first-child { margin-top: 0; } +.markdown-content h1 { + font-size: 1.5rem; +} + +.markdown-content h2 { + font-size: 1.25rem; +} + +.markdown-content h3 { + font-size: 1.125rem; +} + .markdown-content blockquote { - border-left: 4px solid hsl(var(--border)); - padding-left: 1rem; + border-left: 3px solid hsl(var(--primary) / 0.6); + padding: 0.75rem 1rem; margin: 1rem 0; font-style: italic; - color: hsl(var(--muted-foreground)); + background: hsl(var(--muted) / 0.4); + border-radius: 0 0.5rem 0.5rem 0; } .markdown-content table { width: 100%; border-collapse: collapse; margin: 1rem 0; + border-radius: 0.5rem; + overflow: hidden; + border: 1px solid hsl(var(--border) / 0.5); } -.markdown-content th, .markdown-content td { - border: 1px solid hsl(var(--border)); - padding: 0.5rem; +.markdown-content th, +.markdown-content td { + border: 1px solid hsl(var(--border) / 0.3); + padding: 0.75rem; text-align: left; } .markdown-content th { - background-color: hsl(var(--muted)); + background: hsl(var(--muted) / 0.6); font-weight: 600; } -/* Comprehensive scrollbar hiding - Desktop focused */ +.markdown-content tr:nth-child(even) { + background: hsl(var(--muted) / 0.2); +} + +.markdown-content a { + color: hsl(var(--primary)); + text-decoration: none; + border-bottom: 1px solid hsl(var(--primary) / 0.3); + transition: border-color 0.2s ease; +} + +.markdown-content a:hover { + border-bottom-color: hsl(var(--primary)); +} + +/* Inline code styling */ +.markdown-content :not(pre)>code { + background: hsl(var(--muted) / 0.6); + padding: 0.2rem 0.4rem; + border-radius: 0.25rem; + font-size: 0.85em; + color: hsl(var(--primary)); +} + +/* Scrollbar hiding utilities */ .scrollbar-hide { - scrollbar-width: none !important; /* Firefox */ - -ms-overflow-style: none !important; /* IE and Edge */ + scrollbar-width: none !important; + -ms-overflow-style: none !important; } -/* WebKit browsers (Chrome, Safari, Edge Chromium) - Desktop */ .scrollbar-hide::-webkit-scrollbar { display: none !important; width: 0px !important; height: 0px !important; - background: transparent !important; } -.scrollbar-hide::-webkit-scrollbar-track { - display: none !important; - width: 0px !important; - background: transparent !important; +/* Glassmorphism effects */ +.glass { + background: hsl(var(--background) / 0.8); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); } -.scrollbar-hide::-webkit-scrollbar-thumb { - display: none !important; - width: 0px !important; - background: transparent !important; +.glass-strong { + background: hsl(var(--background) / 0.95); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); } -.scrollbar-hide::-webkit-scrollbar-corner { - display: none !important; - background: transparent !important; +/* Message bubble animations */ +@keyframes message-slide-in { + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } } -/* Force hide on all children */ -.scrollbar-hide * { - scrollbar-width: none !important; - -ms-overflow-style: none !important; +.message-animate { + animation: message-slide-in 0.3s ease-out; } -.scrollbar-hide *::-webkit-scrollbar { - display: none !important; - width: 0px !important; - height: 0px !important; +/* Typing indicator animation */ +@keyframes typing-dot { + + 0%, + 60%, + 100% { + transform: translateY(0); + } + + 30% { + transform: translateY(-4px); + } } -/* Desktop-specific hiding using overflow mask */ -.scrollbar-hide-desktop { - overflow: hidden; - position: relative; +.typing-dot { + animation: typing-dot 1.4s ease-in-out infinite; } -.scrollbar-hide-desktop .scroll-content { - overflow-y: scroll; - overflow-x: hidden; - height: 100%; - padding-right: 20px !important; /* Hide scrollbar by pushing it out */ - margin-right: -20px !important; - scrollbar-width: none !important; - -ms-overflow-style: none !important; +.typing-dot:nth-child(2) { + animation-delay: 0.2s; } -.scrollbar-hide-desktop .scroll-content::-webkit-scrollbar { - display: none !important; - width: 0px !important; +.typing-dot:nth-child(3) { + animation-delay: 0.4s; } -/* Ultra-aggressive method for stubborn desktop browsers */ -.no-scrollbar { - scrollbar-width: none !important; - -ms-overflow-style: none !important; +/* Pulse glow animation for agent mode */ +@keyframes pulse-glow { + + 0%, + 100% { + box-shadow: 0 0 5px hsl(var(--primary) / 0.5); + } + + 50% { + box-shadow: 0 0 20px hsl(var(--primary) / 0.8), 0 0 40px hsl(var(--primary) / 0.4); + } } -.no-scrollbar::-webkit-scrollbar { - display: none !important; - width: 0 !important; - height: 0 !important; - opacity: 0 !important; - visibility: hidden !important; +.pulse-glow { + animation: pulse-glow 2s ease-in-out infinite; } -.no-scrollbar::-webkit-scrollbar-track, -.no-scrollbar::-webkit-scrollbar-thumb, -.no-scrollbar::-webkit-scrollbar-corner { - display: none !important; - opacity: 0 !important; - visibility: hidden !important; +/* Shimmer loading effect */ +@keyframes shimmer { + 0% { + background-position: -200% 0; + } + + 100% { + background-position: 200% 0; + } } -/* Specific targeting for sidebar scrollbars */ -[data-sidebar] .scrollbar-hide, -[data-sidebar] .no-scrollbar, -.sidebar .scrollbar-hide, -.sidebar .no-scrollbar { - scrollbar-width: none !important; - -ms-overflow-style: none !important; +.shimmer { + background: linear-gradient(90deg, + hsl(var(--muted) / 0.3) 25%, + hsl(var(--muted) / 0.5) 50%, + hsl(var(--muted) / 0.3) 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; } -[data-sidebar] .scrollbar-hide::-webkit-scrollbar, -[data-sidebar] .no-scrollbar::-webkit-scrollbar, -.sidebar .scrollbar-hide::-webkit-scrollbar, -.sidebar .no-scrollbar::-webkit-scrollbar { - display: none !important; - width: 0px !important; - height: 0px !important; - background: transparent !important; +/* Gradient text */ +.gradient-text { + background: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--accent))); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* Hover lift effect */ +.hover-lift { + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.hover-lift:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px -5px hsl(var(--foreground) / 0.1); +} + +/* Focus ring animation */ +.focus-ring-animate:focus-visible { + outline: none; + box-shadow: 0 0 0 2px hsl(var(--background)), 0 0 0 4px hsl(var(--primary) / 0.5); + transition: box-shadow 0.2s ease; } @custom-variant dark (&:is(.dark *)); @@ -272,105 +386,84 @@ html, body, #root { --color-sidebar-ring: var(--sidebar-ring); } +/* Modern Light Theme - Warm neutrals with violet accent */ :root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.141 0.005 285.823); + --radius: 0.75rem; + --background: oklch(0.985 0.002 280); + --foreground: oklch(0.2 0.02 280); --card: oklch(1 0 0); - --card-foreground: oklch(0.141 0.005 285.823); + --card-foreground: oklch(0.2 0.02 280); --popover: oklch(1 0 0); - --popover-foreground: oklch(0.141 0.005 285.823); - --primary: oklch(0.21 0.006 285.885); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.967 0.001 286.375); - --secondary-foreground: oklch(0.21 0.006 285.885); - --muted: oklch(0.967 0.001 286.375); - --muted-foreground: oklch(0.552 0.016 285.938); - --accent: oklch(0.967 0.001 286.375); - --accent-foreground: oklch(0.21 0.006 285.885); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.92 0.004 286.32); - --input: oklch(0.92 0.004 286.32); - --ring: oklch(0.705 0.015 286.067); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.141 0.005 285.823); - --sidebar-primary: oklch(0.21 0.006 285.885); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.967 0.001 286.375); - --sidebar-accent-foreground: oklch(0.21 0.006 285.885); - --sidebar-border: oklch(0.92 0.004 286.32); - --sidebar-ring: oklch(0.705 0.015 286.067); -} - + --popover-foreground: oklch(0.2 0.02 280); + --primary: oklch(0.55 0.18 280); + --primary-foreground: oklch(0.99 0 0); + --secondary: oklch(0.94 0.01 280); + --secondary-foreground: oklch(0.3 0.03 280); + --muted: oklch(0.94 0.01 280); + --muted-foreground: oklch(0.55 0.03 280); + --accent: oklch(0.6 0.15 300); + --accent-foreground: oklch(0.99 0 0); + --destructive: oklch(0.6 0.2 25); + --border: oklch(0.88 0.01 280); + --input: oklch(0.9 0.01 280); + --ring: oklch(0.55 0.18 280); + --chart-1: oklch(0.55 0.18 280); + --chart-2: oklch(0.6 0.15 180); + --chart-3: oklch(0.65 0.12 120); + --chart-4: oklch(0.7 0.15 45); + --chart-5: oklch(0.6 0.2 30); + --sidebar: oklch(0.98 0.002 280); + --sidebar-foreground: oklch(0.2 0.02 280); + --sidebar-primary: oklch(0.55 0.18 280); + --sidebar-primary-foreground: oklch(0.99 0 0); + --sidebar-accent: oklch(0.94 0.01 280); + --sidebar-accent-foreground: oklch(0.3 0.03 280); + --sidebar-border: oklch(0.9 0.01 280); + --sidebar-ring: oklch(0.55 0.18 280); +} + +/* Modern Dark Theme - Deep slate with violet accent */ .dark { - --background: oklch(0.141 0.005 285.823); - --foreground: oklch(0.985 0 0); - --card: oklch(0.21 0.006 285.885); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.21 0.006 285.885); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.92 0.004 286.32); - --primary-foreground: oklch(0.21 0.006 285.885); - --secondary: oklch(0.274 0.006 286.033); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.274 0.006 286.033); - --muted-foreground: oklch(0.705 0.015 286.067); - --accent: oklch(0.274 0.006 286.033); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.552 0.016 285.938); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.21 0.006 285.885); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.274 0.006 286.033); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.552 0.016 285.938); + --background: oklch(0.15 0.015 280); + --foreground: oklch(0.95 0.005 280); + --card: oklch(0.2 0.02 280); + --card-foreground: oklch(0.95 0.005 280); + --popover: oklch(0.2 0.02 280); + --popover-foreground: oklch(0.95 0.005 280); + --primary: oklch(0.65 0.15 280); + --primary-foreground: oklch(0.15 0.02 280); + --secondary: oklch(0.25 0.02 280); + --secondary-foreground: oklch(0.9 0.01 280); + --muted: oklch(0.25 0.02 280); + --muted-foreground: oklch(0.65 0.02 280); + --accent: oklch(0.7 0.12 300); + --accent-foreground: oklch(0.15 0.02 280); + --destructive: oklch(0.65 0.18 25); + --border: oklch(0.25 0.02 280); + --input: oklch(0.25 0.02 280); + --ring: oklch(0.65 0.15 280); + --chart-1: oklch(0.65 0.15 280); + --chart-2: oklch(0.7 0.12 180); + --chart-3: oklch(0.75 0.1 120); + --chart-4: oklch(0.8 0.12 45); + --chart-5: oklch(0.7 0.18 30); + --sidebar: oklch(0.18 0.02 280); + --sidebar-foreground: oklch(0.95 0.005 280); + --sidebar-primary: oklch(0.65 0.15 280); + --sidebar-primary-foreground: oklch(0.15 0.02 280); + --sidebar-accent: oklch(0.25 0.02 280); + --sidebar-accent-foreground: oklch(0.95 0.005 280); + --sidebar-border: oklch(0.25 0.02 280); + --sidebar-ring: oklch(0.65 0.15 280); } @layer base { * { @apply border-border outline-ring/50; } + body { @apply bg-background text-foreground; - } -} - - -@layer base { - :root { - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); - } - - .dark { - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.439 0 0); + font-feature-settings: "cv02", "cv03", "cv04", "cv11"; } } \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 57f1971..4078410 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,7 +8,6 @@ import AppLayout from "./components/app/app-layout"; import SettingsPage from "./pages/settings"; import HomePage from "./pages/home"; import { Toaster } from "./components/ui/sonner"; -import PermissionDialog from "./components/app/permission-dialog"; const router = createBrowserRouter([ { @@ -41,9 +40,8 @@ function App() { return ( - + - diff --git a/src/components/app/agent-mode.tsx b/src/components/app/agent-mode.tsx index 598dbe5..f60e54e 100644 --- a/src/components/app/agent-mode.tsx +++ b/src/components/app/agent-mode.tsx @@ -2,17 +2,90 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui import { Input } from "../ui/input"; import { Label } from "../ui/label"; import { Badge } from "../ui/badge"; -import { ScrollArea } from "../ui/scroll-area"; import { Switch } from "../ui/switch"; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "../ui/collapsible"; import { toast } from "sonner"; -import { FaRobot, FaFolder, FaTerminal, FaRocket, FaCog, FaChevronDown, FaShieldAlt } from "react-icons/fa"; +import { + FaRobot, + FaFolder, + FaTerminal, + FaRocket, + FaShieldAlt, + FaBrain, + FaCheck, + FaFileAlt, + FaDesktop +} from "react-icons/fa"; import { useAgent } from "../../contexts/AgentContext"; -import { useState } from "react"; + +const capabilities = [ + { + id: "overview", + icon: , + title: "How Agent Mode Works", + description: "When enabled, simply chat with natural language requests and the AI will automatically understand your intent and perform actions.", + examples: [ + "Open Chrome browser", + "Run npm install in current directory", + "List all running processes", + "Create a new folder called 'test'" + ] + }, + { + id: "file-ops", + icon: , + title: "File Operations", + badge: "Enhanced", + description: "Manage files and directories through natural language commands.", + features: [ + "Copy, move, rename, delete files/folders", + "Create new directories", + "Search files with patterns", + "Read and write file contents", + "Open files with default apps" + ] + }, + { + id: "terminal", + icon: , + title: "Terminal Commands", + badge: "Permission Required", + description: "Execute terminal and shell commands with user permission.", + features: [ + "Run build scripts and automation", + "Install packages (npm, pip, etc.)", + "Git operations and version control", + "System administration tasks" + ] + }, + { + id: "apps", + icon: , + title: "Application Control", + description: "Launch and manage applications on your system.", + features: [ + "Launch installed applications", + "List all installed apps", + "Pass arguments to programs", + "View running processes" + ] + }, + { + id: "processes", + icon: , + title: "Process Management", + badge: "Advanced", + description: "Monitor and manage system processes.", + features: [ + "View all running processes", + "Monitor CPU and memory usage", + "Terminate specific processes", + "Manage system resources" + ] + } +]; export default function AgentMode() { const { isAgentActive, workingDirectory, setAgentActive, setWorkingDirectory } = useAgent(); - const [expandedSections, setExpandedSections] = useState(["overview"]); const handleWorkingDirectoryChange = (newPath: string) => { setWorkingDirectory(newPath); @@ -27,284 +100,145 @@ export default function AgentMode() { }; return ( - - - - - Agent Mode - {isAgentActive && Active} - - - Enable autonomous AI agent functionality for intelligent task execution - - - - -
- {/* Enable/Disable Toggle */} -
-
- -
- Turn on autonomous AI agent functionality -
+
+ {/* Main Toggle Card */} + + +
+
+
+
- -
- - {/* Working Directory */} -
- - handleWorkingDirectoryChange(e.target.value)} - disabled={!isAgentActive} - /> -
- Set the base directory where the agent will operate +
+ + Agent Mode + {isAgentActive && ( + + + Active + + )} + + + Enable autonomous AI agent for intelligent task execution +
+ +
+ + + {/* Working Directory */} +
+ + handleWorkingDirectoryChange(e.target.value)} + disabled={!isAgentActive} + className="bg-background" + /> +

+ The base directory where the agent will perform file operations +

+
- {/* Capabilities Overview */} - {isAgentActive && ( -
- {/* Overview Section */} - { - setExpandedSections(open - ? [...expandedSections, "overview"] - : expandedSections.filter(s => s !== "overview") - ); - }} - > - -
- - How Agent Mode Works -
- -
- -
-
-
When agent mode is enabled, simply chat with natural language requests. The AI will automatically understand your intent and perform the appropriate actions.
- -
-
Quick Examples:
-
-
â€ĸ "Open Chrome browser"
-
â€ĸ "Run npm install in current directory"
-
â€ĸ "List all running processes"
-
â€ĸ "Create a new folder called 'test'"
-
-
-
-
-
-
- - {/* File Operations */} - { - setExpandedSections(open - ? [...expandedSections, "file-ops"] - : expandedSections.filter(s => s !== "file-ops") - ); - }} - > - -
- - File & Directory Operations -
- -
- -
-
-
- Enhanced File Explorer -
- â€ĸ Copy, move, rename, and delete files/folders
- â€ĸ Create new directories
- â€ĸ Search files with regex patterns
- â€ĸ Read and write file contents
- â€ĸ Open files with default applications -
-
-
- Example: "Copy all .js files to backup folder" -
-
-
-
-
+ {/* Security Notice */} +
+ +
+

Security First

+

+ All potentially dangerous operations require your explicit permission. + The agent will never execute harmful commands without your approval. +

+
+
+ + - {/* Terminal Commands */} - { - setExpandedSections(open - ? [...expandedSections, "terminal"] - : expandedSections.filter(s => s !== "terminal") - ); - }} - > - -
- - Terminal Command Execution - With Permission -
- -
- -
-
-
-
- - Permission Required -
-
- â€ĸ Execute terminal/shell commands
- â€ĸ Run build scripts and automation
- â€ĸ Install packages and dependencies
- â€ĸ Git operations and version control
- â€ĸ System administration tasks -
-
-
- âš ī¸ Dangerous commands are blocked for safety -
-
- Example: "Run npm test in the project folder" -
-
+ {/* Capabilities Grid - Only show when active */} + {isAgentActive && ( +
+ {capabilities.map((cap) => ( + + +
+
+
+ {cap.icon}
- - - - {/* Application Control */} - { - setExpandedSections(open - ? [...expandedSections, "apps"] - : expandedSections.filter(s => s !== "apps") - ); - }} - > - -
- - Application Control +
+ {cap.title} + {cap.badge && ( + + {cap.badge} + + )}
- - - -
-
-
- Launch & Manage Apps -
- â€ĸ Launch installed applications
- â€ĸ List all installed apps
- â€ĸ Pass arguments to applications
- â€ĸ View running processes
- â€ĸ Terminate processes (with permission) -
-
-
- Example: "Open Visual Studio Code with the current folder" -
+
+
+ + +

+ {cap.description} +

+ {cap.examples ? ( +
+

Quick Examples:

+ {cap.examples.map((example, idx) => ( +
+ + {example}
-
-
- - - {/* Process Management */} - { - setExpandedSections(open - ? [...expandedSections, "processes"] - : expandedSections.filter(s => s !== "processes") - ); - }} - > - -
- - Process Management - Advanced -
- -
- -
-
-
-
- - High Permission Required -
-
- â€ĸ View all running processes
- â€ĸ Monitor CPU and memory usage
- â€ĸ Terminate specific processes by PID
- â€ĸ Manage system resources -
-
-
- 🔒 Requires explicit user permission for each action -
-
- Example: "Show me all Chrome processes" -
+ ))} +
+ ) : ( +
+ {cap.features?.map((feature, idx) => ( +
+ + {feature}
-
- - - - {/* Security Notice */} -
-
- -
-

Security First

-

- All potentially dangerous operations require your explicit permission. - The agent will never execute harmful commands without your approval. -

-
+ ))}
-
-
- )} -
- - - + )} + + + ))} +
+ )} + + {/* Inactive State */} + {!isAgentActive && ( + + +
+ +
+

Agent Mode is Disabled

+

+ Enable Agent Mode to unlock autonomous AI capabilities including file operations, + terminal commands, and application control. +

+
+
+ )} +
); } diff --git a/src/components/app/app-layout.tsx b/src/components/app/app-layout.tsx index c6a3476..8f5d0d7 100644 --- a/src/components/app/app-layout.tsx +++ b/src/components/app/app-layout.tsx @@ -16,12 +16,16 @@ import { import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Switch } from "@/components/ui/switch"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; import { toast } from "sonner"; import { FaPlus, FaCog, FaTrash, - FaRobot + FaRobot, + FaComments, + FaClock, + FaSearch } from "react-icons/fa"; import { RxCross2 } from "react-icons/rx"; import { VscChromeMinimize } from "react-icons/vsc"; @@ -34,8 +38,6 @@ import { useAgent } from "@/contexts/AgentContext"; import ThemeToggle from "./theme-toggle"; import type { ChatWithLastMessage } from "@/lib/types"; - - interface AppLayoutProps { children: React.ReactNode; } @@ -50,11 +52,27 @@ export default function AppLayout({ children }: AppLayoutProps) { const [chats, setChats] = useState([]); const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(""); + const [deleteConfirm, setDeleteConfirm] = useState(null); useEffect(() => { loadChats(); }, []); + // Reload chats when navigating to a chat that's not in our list (e.g., created from home page) + useEffect(() => { + const chatIdFromUrl = location.pathname.startsWith('/chat/') + ? location.pathname.split('/chat/')[1] + : null; + + if (chatIdFromUrl && !loading) { + const chatExists = chats.some(chat => chat.id === chatIdFromUrl); + if (!chatExists) { + loadChats(); + } + } + }, [location.pathname, chats, loading]); + const loadChats = async () => { try { setLoading(true); @@ -105,7 +123,7 @@ export default function AppLayout({ children }: AppLayoutProps) { ...prev ]); navigate(`/chat/${newChat.id}`); - toast.success('New chat created!'); + toast.success('New chat created!', { icon: }); } catch (error) { console.error('Failed to create chat:', error); toast.error('Failed to create new chat. Please try again.'); @@ -114,217 +132,331 @@ export default function AppLayout({ children }: AppLayoutProps) { const handleDeleteChat = async (chatId: string, e: React.MouseEvent) => { e.stopPropagation(); - try { - await deleteChatApi(chatId); - setChats(prev => prev.filter(chat => chat.id !== chatId)); - // Navigate away if we're currently viewing the deleted chat - const currentChatId = location.pathname.split('/').pop(); - if (currentChatId === chatId) { - navigate('/'); + if (deleteConfirm === chatId) { + try { + await deleteChatApi(chatId); + setChats(prev => prev.filter(chat => chat.id !== chatId)); + setDeleteConfirm(null); + + const currentChatId = location.pathname.split('/').pop(); + if (currentChatId === chatId) { + navigate('/'); + } + toast.success('Chat deleted successfully'); + } catch (error) { + console.error('Failed to delete chat:', error); + toast.error('Failed to delete chat. Please try again.'); } - toast.success('Chat deleted successfully'); - } catch (error) { - console.error('Failed to delete chat:', error); - toast.error('Failed to delete chat. Please try again.'); + } else { + setDeleteConfirm(chatId); + setTimeout(() => setDeleteConfirm(null), 3000); } }; + const filteredChats = chats.filter(chat => + chat.title.toLowerCase().includes(searchQuery.toLowerCase()) || + (chat.last_message && chat.last_message.toLowerCase().includes(searchQuery.toLowerCase())) + ); + const currentChatId = location.pathname.split('/').pop(); + const activeChat = chats.find(chat => chat.id === currentChatId); + + // Group chats by date + const groupedChats = filteredChats.reduce((groups, chat) => { + const date = new Date(chat.last_message_time || chat.updated_at); + const now = new Date(); + const diff = now.getTime() - date.getTime(); + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + + let group = 'Older'; + if (days === 0) group = 'Today'; + else if (days === 1) group = 'Yesterday'; + else if (days < 7) group = 'This Week'; + else if (days < 30) group = 'This Month'; + + if (!groups[group]) groups[group] = []; + groups[group].push(chat); + return groups; + }, {} as Record); + + const groupOrder = ['Today', 'Yesterday', 'This Week', 'This Month', 'Older']; return ( - <> - - -
- -

ChatMe

-
+ + <> + + + {/* Logo */} +
+
+ +
+
+

ChatMe

+

AI Assistant

+
+
- -
- - - - - Chat History - - -
- - {loading ? ( -
-
Loading chats...
-
- ) : chats.length === 0 ? ( -
-

- No chats yet -

-

- Create your first chat to get started -

-
- ) : ( - chats.map((chat) => ( - - navigate(`/chat/${chat.id}`)} - isActive={currentChatId === chat.id} - className="w-full p-3 h-auto flex-col items-start rounded-lg hover:bg-muted/80 transition-colors relative" - > -
-
-
- - {chat.title} - - {chat.unread_count > 0 && ( - - {chat.unread_count} - - )} -
-

- {chat.last_message || "No messages yet"} -

- - {formatTime(chat.last_message_time || chat.updated_at)} - -
- - {/* Delete Button - positioned absolutely */} -
-
handleDeleteChat(chat.id, e)} + {/* New Chat Button */} + + + {/* Search */} +
+ + setSearchQuery(e.target.value)} + className="w-full h-9 pl-9 pr-3 text-sm bg-muted/50 border border-border/40 rounded-xl focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary/30 transition-all placeholder:text-muted-foreground/50" + /> +
+ + + +
+ {loading ? ( +
+
+

Loading chats...

+
+ ) : filteredChats.length === 0 ? ( +
+
+ +
+

+ {searchQuery ? "No chats found" : "No chats yet"} +

+

+ {searchQuery ? "Try a different search term" : "Start a new conversation"} +

+
+ ) : ( + + {groupOrder.map(group => { + const groupChats = groupedChats[group]; + if (!groupChats || groupChats.length === 0) return null; + + return ( + + + {group} + + + {groupChats.map((chat) => ( + + navigate(`/chat/${chat.id}`)} + isActive={currentChatId === chat.id} + className="w-full p-3 h-auto rounded-xl transition-all duration-200 relative overflow-hidden" > - -
-
-
- - - )) - )} +
+
+ {/* Title row */} +
+ + {chat.title} + + {chat.unread_count > 0 && ( + + {chat.unread_count} + + )} +
+ + {/* Last message */} +

+ {chat.last_message || "Start a conversation..."} +

+ + {/* Time */} +
+ + {formatTime(chat.last_message_time || chat.updated_at)} +
+
+ + {/* Delete Button */} + + +
handleDeleteChat(chat.id, e)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleDeleteChat(chat.id, e as any); + } + }} + > + +
+
+ +

+ {deleteConfirm === chat.id ? "Click again to confirm" : "Delete chat"} +

+
+
+
+ + + ))} + + + ); + })} -
- - - - - - - - navigate('/settings')} - isActive={location.pathname === '/settings'} - className="gap-3 p-3 rounded-lg cursor-pointer transition-colors" - > - - Settings - - - - - - - -
-
- -
- -
- -

- {currentChatId ? - chats.find(chat => chat.id === currentChatId)?.title || 'Chat' : - 'ChatMe' - } -

-
+ )} +
+ - {/* Agent Mode Toggle */} -
- Agent - {isAgentActive && ON} - { - setAgentActive(checked); - toast.info(checked ? "Agent mode enabled" : "Agent mode disabled"); - }} - className="scale-75" - /> -
- - {/* Hide window controls on mobile */} - {!isMobile && ( -
- - - - + + + + navigate('/settings')} + isActive={location.pathname === '/settings'} + className="gap-3 p-3 rounded-xl cursor-pointer transition-all duration-200 hover:bg-muted/80" + > +
+ +
+
+ Settings + Configure app & AI +
+
+
+
+
+ + + + {/* Header */} +
+
+ +
+ + {/* Chat title */} +
+ {activeChat ? ( + <> +
+ +
+
+

+ {activeChat.title} +

+

+ {activeChat.last_message || "New conversation"} +

+
+ + ) : ( + <> +
+ +
+
+

Welcome to ChatMe

+

Start a new conversation

+
+ + )}
- )} - {/* Mobile-only theme toggle */} - {isMobile && ( -
- + {/* Agent Mode Toggle */} +
+ Agent Mode + {isAgentActive && ( + + + ON + + )} + { + setAgentActive(checked); + toast.info(checked ? "Agent mode enabled" : "Agent mode disabled"); + }} + className="scale-75 data-[state=checked]:bg-primary" + />
- )} -
-
- {children} -
-
- + + {/* Window controls */} + {!isMobile && ( +
+ + + + +
+ )} + + {isMobile && ( +
+ +
+ )} + + + {/* Main content */} +
+ {children} +
+ + + ); } diff --git a/src/components/app/input-box.tsx b/src/components/app/input-box.tsx index ee29225..e0aaf69 100644 --- a/src/components/app/input-box.tsx +++ b/src/components/app/input-box.tsx @@ -1,7 +1,7 @@ import { useState, useRef, KeyboardEvent, useEffect, forwardRef, useImperativeHandle } from "react"; import { Button } from "../ui/button"; import { Textarea } from "../ui/textarea"; -import { FaArrowRight, FaPaperclip, FaMicrophone, FaTimes, FaStop, FaKeyboard } from "react-icons/fa"; +import { FaArrowRight, FaPaperclip, FaMicrophone, FaTimes, FaStop, FaKeyboard, FaMagic } from "react-icons/fa"; import { useSpeechRecognition } from "../../hooks/use-speech-recognition"; import { toast } from "sonner"; import { @@ -10,6 +10,7 @@ import { TooltipProvider, TooltipTrigger, } from "../ui/tooltip"; +import { Badge } from "../ui/badge"; interface InputBoxProps { onSendMessage?: (message: string, images?: string[]) => void; @@ -25,11 +26,11 @@ const InputBox = forwardRef(({ onSendMessage, disabl const [message, setMessage] = useState(""); const [, setIsTyping] = useState(false); const [selectedImages, setSelectedImages] = useState([]); + const [isFocused, setIsFocused] = useState(false); const textareaRef = useRef(null); const fileInputRef = useRef(null); const [lastCommand, setLastCommand] = useState(""); - // Speech recognition setup const { isListening, transcript, @@ -49,7 +50,6 @@ const InputBox = forwardRef(({ onSendMessage, disabl interimResults: true }); - // Expose methods to parent component useImperativeHandle(ref, () => ({ focus: () => { textareaRef.current?.focus(); @@ -59,23 +59,19 @@ const InputBox = forwardRef(({ onSendMessage, disabl } })); - // Auto-resize textarea when message changes useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = "auto"; - textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 120)}px`; + textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 160)}px`; } }, [message]); - // Global keyboard shortcuts useEffect(() => { const handleGlobalKeyDown = (e: globalThis.KeyboardEvent) => { - // Ctrl+K or Cmd+K to focus input if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); textareaRef.current?.focus(); } - // Ctrl+R or Cmd+R to repeat last command (only when focused) if ((e.ctrlKey || e.metaKey) && e.key === 'r' && document.activeElement === textareaRef.current) { e.preventDefault(); if (lastCommand) { @@ -99,8 +95,7 @@ const InputBox = forwardRef(({ onSendMessage, disabl setMessage(""); setSelectedImages([]); setIsTyping(false); - resetTranscript(); // Clear speech recognition transcript - // Reset textarea height + resetTranscript(); if (textareaRef.current) { textareaRef.current.style.height = "auto"; } @@ -112,7 +107,6 @@ const InputBox = forwardRef(({ onSendMessage, disabl toast.error("Speech recognition is not supported in your browser"); return; } - if (isListening) { stopListening(); } else { @@ -136,8 +130,7 @@ const InputBox = forwardRef(({ onSendMessage, disabl reader.readAsDataURL(file); } }); - - // Reset input + if (fileInputRef.current) { fileInputRef.current.value = ''; } @@ -157,170 +150,216 @@ const InputBox = forwardRef(({ onSendMessage, disabl const handleInputChange = (e: React.ChangeEvent) => { setMessage(e.target.value); setIsTyping(e.target.value.length > 0); - - // Auto-resize textarea + if (textareaRef.current) { textareaRef.current.style.height = "auto"; - textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 120)}px`; + textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 160)}px`; } }; + const hasContent = message.trim().length > 0 || selectedImages.length > 0; + const charCount = message.length; + const maxChars = 4000; + return ( -
-
- {/* Image preview */} - {selectedImages.length > 0 && ( -
-
- {selectedImages.map((image, index) => ( -
- {`Upload - -
- ))} + +
+
+ {/* Image preview */} + {selectedImages.length > 0 && ( +
+
+ {selectedImages.map((image, index) => ( +
+ {`Upload + +
+ ))} +
-
- )} + )} -
- {/* Attachment button */} - - - {/* Hidden file input */} - + {/* Attachment button */} + + + + + +

Attach images

+
+
- {/* Text input area */} -
-