diff --git a/Cargo.lock b/Cargo.lock index 0e6ee59022..1616f60aad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4123,6 +4123,7 @@ dependencies = [ "tauri-plugin-listener2", "tauri-plugin-local-stt", "tauri-plugin-misc", + "tauri-plugin-network", "tauri-plugin-notification", "tauri-plugin-opener", "tauri-plugin-os", @@ -15456,6 +15457,23 @@ dependencies = [ "vergen-gix", ] +[[package]] +name = "tauri-plugin-network" +version = "0.1.0" +dependencies = [ + "network", + "serde", + "specta", + "specta-typescript", + "tauri", + "tauri-plugin", + "tauri-plugin-windows", + "tauri-specta", + "thiserror 2.0.17", + "tokio", + "tracing", +] + [[package]] name = "tauri-plugin-notification" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9469c0f339..40447fa9b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -126,6 +126,7 @@ tauri-plugin-listener2 = { path = "plugins/listener2" } tauri-plugin-local-llm = { path = "plugins/local-llm" } tauri-plugin-local-stt = { path = "plugins/local-stt" } tauri-plugin-misc = { path = "plugins/misc" } +tauri-plugin-network = { path = "plugins/network" } tauri-plugin-notification = { path = "plugins/notification" } tauri-plugin-permissions = { path = "plugins/permissions" } tauri-plugin-sfx = { path = "plugins/sfx" } diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index f8911d2ff6..fe7d88521c 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -42,6 +42,7 @@ tauri-plugin-listener = { workspace = true } tauri-plugin-listener2 = { workspace = true } tauri-plugin-local-stt = { workspace = true } tauri-plugin-misc = { workspace = true } +tauri-plugin-network = { workspace = true } tauri-plugin-notification = { workspace = true } tauri-plugin-opener = { workspace = true } tauri-plugin-os = { workspace = true } diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index 50505ce86a..cf5bc31245 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -105,6 +105,7 @@ ] }, "misc:default", + "network:default", "os:default", "detect:default", "permissions:default", diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 79377f9b4b..e45dd00623 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -71,6 +71,7 @@ pub async fn main() { .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_process::init()) .plugin(tauri_plugin_misc::init()) + .plugin(tauri_plugin_network::init()) .plugin(tauri_plugin_template::init()) .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_detect::init()) diff --git a/apps/desktop/src/contexts/network.tsx b/apps/desktop/src/contexts/network.tsx new file mode 100644 index 0000000000..20c2d99d72 --- /dev/null +++ b/apps/desktop/src/contexts/network.tsx @@ -0,0 +1,77 @@ +import React, { createContext, useContext, useEffect, useState } from "react"; + +import { commands, events } from "@hypr/plugin-network"; + +interface NetworkContextValue { + isOnline: boolean; +} + +const NetworkContext = createContext(null); + +export const NetworkProvider = ({ + children, +}: { + children: React.ReactNode; +}) => { + const [isOnline, setIsOnline] = useState(true); + + useEffect(() => { + commands + .isOnline() + .then((result) => { + if (result.status === "ok") { + setIsOnline(result.data); + } else { + console.error("Failed to check network status:", result.error); + setIsOnline(false); + } + }) + .catch((err) => { + console.error("Failed to check network status:", err); + setIsOnline(false); + }); + }, []); + + useEffect(() => { + let unlisten: (() => void) | undefined; + let cancelled = false; + + events.networkEvent + .listen(({ payload }) => { + if (payload.type === "statusChanged") { + setIsOnline(payload.is_online); + } + }) + .then((fn) => { + if (cancelled) { + fn(); + } else { + unlisten = fn; + } + }) + .catch((err) => { + console.error("Failed to setup network event listener:", err); + }); + + return () => { + cancelled = true; + unlisten?.(); + }; + }, []); + + return ( + + {children} + + ); +}; + +export const useNetwork = () => { + const context = useContext(NetworkContext); + + if (!context) { + throw new Error("'useNetwork' must be used within a 'NetworkProvider'"); + } + + return context; +}; diff --git a/plugins/network/.gitignore b/plugins/network/.gitignore new file mode 100644 index 0000000000..3c3629e647 --- /dev/null +++ b/plugins/network/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/plugins/network/Cargo.toml b/plugins/network/Cargo.toml new file mode 100644 index 0000000000..bd8800d5cf --- /dev/null +++ b/plugins/network/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "tauri-plugin-network" +version = "0.1.0" +authors = ["You"] +edition = "2021" +exclude = ["/js", "/node_modules"] +links = "tauri-plugin-network" +description = "" + +[build-dependencies] +tauri-plugin = { workspace = true, features = ["build"] } + +[dev-dependencies] +specta-typescript = { workspace = true } + +[dependencies] +hypr-network = { workspace = true } + +tauri = { workspace = true, features = ["specta", "test"] } +tauri-plugin-windows = { workspace = true } + +specta = { workspace = true } +tauri-specta = { workspace = true, features = ["derive", "typescript"] } + +serde = { workspace = true } +thiserror = { workspace = true } + +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "time"] } +tracing = { workspace = true } diff --git a/plugins/network/build.rs b/plugins/network/build.rs new file mode 100644 index 0000000000..0d9b53c7f0 --- /dev/null +++ b/plugins/network/build.rs @@ -0,0 +1,5 @@ +const COMMANDS: &[&str] = &["is_online"]; + +fn main() { + tauri_plugin::Builder::new(COMMANDS).build(); +} diff --git a/plugins/network/js/bindings.gen.ts b/plugins/network/js/bindings.gen.ts new file mode 100644 index 0000000000..adc0e363a0 --- /dev/null +++ b/plugins/network/js/bindings.gen.ts @@ -0,0 +1,95 @@ +// @ts-nocheck + + +// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. + +/** user-defined commands **/ + + +export const commands = { +async isOnline() : Promise> { + try { + return { status: "ok", data: await TAURI_INVOKE("plugin:network|is_online") }; +} catch (e) { + if(e instanceof Error) throw e; + else return { status: "error", error: e as any }; +} +} +} + +/** user-defined events **/ + + +export const events = __makeEvents__<{ +networkEvent: NetworkEvent +}>({ +networkEvent: "plugin:network:network-event" +}) + +/** user-defined constants **/ + + + +/** user-defined types **/ + +export type NetworkEvent = { type: "statusChanged"; is_online: boolean } + +/** tauri-specta globals **/ + +import { + invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, +} from "@tauri-apps/api/core"; +import * as TAURI_API_EVENT from "@tauri-apps/api/event"; +import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; + +type __EventObj__ = { + listen: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback, + ) => ReturnType>; + emit: null extends T + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; +}; + +export type Result = + | { status: "ok"; data: T } + | { status: "error"; error: E }; + +function __makeEvents__>( + mappings: Record, +) { + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; + + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case "listen": + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case "once": + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case "emit": + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + }, + ); +} diff --git a/plugins/network/js/index.ts b/plugins/network/js/index.ts new file mode 100644 index 0000000000..a96e122f03 --- /dev/null +++ b/plugins/network/js/index.ts @@ -0,0 +1 @@ +export * from "./bindings.gen"; diff --git a/plugins/network/package.json b/plugins/network/package.json new file mode 100644 index 0000000000..d1b95d1f58 --- /dev/null +++ b/plugins/network/package.json @@ -0,0 +1,11 @@ +{ + "name": "@hypr/plugin-network", + "private": true, + "main": "./js/index.ts", + "scripts": { + "codegen": "cargo test -p tauri-plugin-network" + }, + "dependencies": { + "@tauri-apps/api": "^2.9.0" + } +} diff --git a/plugins/network/permissions/autogenerated/commands/is_online.toml b/plugins/network/permissions/autogenerated/commands/is_online.toml new file mode 100644 index 0000000000..4e6cc33f51 --- /dev/null +++ b/plugins/network/permissions/autogenerated/commands/is_online.toml @@ -0,0 +1,13 @@ +# Automatically generated - DO NOT EDIT! + +"$schema" = "../../schemas/schema.json" + +[[permission]] +identifier = "allow-is-online" +description = "Enables the is_online command without any pre-configured scope." +commands.allow = ["is_online"] + +[[permission]] +identifier = "deny-is-online" +description = "Denies the is_online command without any pre-configured scope." +commands.deny = ["is_online"] diff --git a/plugins/network/permissions/autogenerated/reference.md b/plugins/network/permissions/autogenerated/reference.md new file mode 100644 index 0000000000..a9de9a2ea9 --- /dev/null +++ b/plugins/network/permissions/autogenerated/reference.md @@ -0,0 +1,43 @@ +## Default Permission + +Default permissions for the plugin + +#### This default permission set includes the following: + +- `allow-is-online` + +## Permission Table + + + + + + + + + + + + + + + + + +
IdentifierDescription
+ +`network:allow-is-online` + + + +Enables the is_online command without any pre-configured scope. + +
+ +`network:deny-is-online` + + + +Denies the is_online command without any pre-configured scope. + +
diff --git a/plugins/network/permissions/default.toml b/plugins/network/permissions/default.toml new file mode 100644 index 0000000000..c7513b4081 --- /dev/null +++ b/plugins/network/permissions/default.toml @@ -0,0 +1,5 @@ +[default] +description = "Default permissions for the plugin" +permissions = [ + "allow-is-online", +] diff --git a/plugins/network/permissions/schemas/schema.json b/plugins/network/permissions/schemas/schema.json new file mode 100644 index 0000000000..d7bfbccc35 --- /dev/null +++ b/plugins/network/permissions/schemas/schema.json @@ -0,0 +1,318 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use `

` headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the is_online command without any pre-configured scope.", + "type": "string", + "const": "allow-is-online", + "markdownDescription": "Enables the is_online command without any pre-configured scope." + }, + { + "description": "Denies the is_online command without any pre-configured scope.", + "type": "string", + "const": "deny-is-online", + "markdownDescription": "Denies the is_online command without any pre-configured scope." + }, + { + "description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-is-online`", + "type": "string", + "const": "default", + "markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-is-online`" + } + ] + } + } +} \ No newline at end of file diff --git a/plugins/network/src/commands.rs b/plugins/network/src/commands.rs new file mode 100644 index 0000000000..b7e391984a --- /dev/null +++ b/plugins/network/src/commands.rs @@ -0,0 +1,7 @@ +use tauri::{AppHandle, Runtime}; + +#[tauri::command] +#[specta::specta] +pub async fn is_online(_app: AppHandle) -> Result { + Ok(hypr_network::is_online().await) +} diff --git a/plugins/network/src/error.rs b/plugins/network/src/error.rs new file mode 100644 index 0000000000..6be397e2f9 --- /dev/null +++ b/plugins/network/src/error.rs @@ -0,0 +1,14 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Network check failed: {0}")] + NetworkCheckFailed(String), +} + +impl serde::Serialize for Error { + fn serialize(&self, serializer: S) -> Result + where + S: serde::ser::Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} diff --git a/plugins/network/src/events.rs b/plugins/network/src/events.rs new file mode 100644 index 0000000000..07a778e5b5 --- /dev/null +++ b/plugins/network/src/events.rs @@ -0,0 +1,15 @@ +#[macro_export] +macro_rules! common_event_derives { + ($item:item) => { + #[derive(serde::Serialize, Clone, specta::Type, tauri_specta::Event)] + $item + }; +} + +common_event_derives! { + #[serde(tag = "type")] + pub enum NetworkEvent { + #[serde(rename = "statusChanged")] + StatusChanged { is_online: bool }, + } +} diff --git a/plugins/network/src/handler.rs b/plugins/network/src/handler.rs new file mode 100644 index 0000000000..28ccdd5cca --- /dev/null +++ b/plugins/network/src/handler.rs @@ -0,0 +1,38 @@ +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use tauri::{AppHandle, EventTarget, Manager, Runtime}; +use tauri_plugin_windows::WindowImpl; +use tauri_specta::Event; + +use crate::NetworkEvent; + +pub async fn setup(app: &AppHandle) -> Result<(), Box> { + let app_handle = app.app_handle().clone(); + let initial_status = hypr_network::is_online().await; + let last_status = Arc::new(AtomicBool::new(initial_status)); + + tokio::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(5)); + + loop { + interval.tick().await; + + let is_online = hypr_network::is_online().await; + let previous = last_status.swap(is_online, Ordering::SeqCst); + + if is_online != previous { + let event = NetworkEvent::StatusChanged { is_online }; + let _ = event.emit_to( + &app_handle, + EventTarget::AnyLabel { + label: tauri_plugin_windows::AppWindow::Main.label(), + }, + ); + } + } + }); + + Ok(()) +} diff --git a/plugins/network/src/lib.rs b/plugins/network/src/lib.rs new file mode 100644 index 0000000000..d7c771d89f --- /dev/null +++ b/plugins/network/src/lib.rs @@ -0,0 +1,59 @@ +use tauri::Manager; + +mod commands; +mod error; +mod events; +mod handler; + +pub use error::*; +pub use events::*; + +const PLUGIN_NAME: &str = "network"; + +fn make_specta_builder() -> tauri_specta::Builder { + tauri_specta::Builder::::new() + .plugin_name(PLUGIN_NAME) + .commands(tauri_specta::collect_commands![ + commands::is_online::, + ]) + .events(tauri_specta::collect_events![NetworkEvent]) + .error_handling(tauri_specta::ErrorHandlingMode::Result) +} + +pub fn init() -> tauri::plugin::TauriPlugin { + let specta_builder = make_specta_builder(); + + tauri::plugin::Builder::new(PLUGIN_NAME) + .invoke_handler(specta_builder.invoke_handler()) + .setup(move |app, _api| { + specta_builder.mount_events(app); + + let app_handle = app.app_handle().clone(); + tauri::async_runtime::spawn(async move { + if let Err(e) = handler::setup(&app_handle).await { + tracing::error!("failed to setup network handler: {}", e); + } + }); + + Ok(()) + }) + .build() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn export_types() { + make_specta_builder::() + .export( + specta_typescript::Typescript::default() + .header("// @ts-nocheck\n\n") + .formatter(specta_typescript::formatter::prettier) + .bigint(specta_typescript::BigIntExportBehavior::Number), + "./js/bindings.gen.ts", + ) + .unwrap() + } +} diff --git a/plugins/network/tsconfig.json b/plugins/network/tsconfig.json new file mode 100644 index 0000000000..4eb37fee05 --- /dev/null +++ b/plugins/network/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.base.json" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 509d95befe..4aad43e1cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -400,10 +400,10 @@ importers: version: 10.1.0 '@tanstack/react-router-devtools': specifier: ^1.139.3 - version: 1.139.3(@tanstack/react-router@1.139.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.3)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10)(tsx@4.20.6)(yaml@2.8.1) + version: 1.139.3(@tanstack/react-router@1.139.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.3)(@types/node@24.10.1)(csstype@3.2.3)(jiti@1.21.7)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10)(tsx@4.20.6)(yaml@2.8.1) '@tanstack/router-plugin': specifier: ^1.139.3 - version: 1.139.3(@tanstack/react-router@1.139.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) + version: 1.139.3(@tanstack/react-router@1.139.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) '@tauri-apps/cli': specifier: ^2.9.4 version: 2.9.4 @@ -430,7 +430,7 @@ importers: version: 2.0.3 '@vitejs/plugin-react': specifier: ^4.7.0 - version: 4.7.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) + version: 4.7.0(vite@7.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) autoprefixer: specifier: ^10.4.22 version: 10.4.22(postcss@8.5.6) @@ -451,10 +451,10 @@ importers: version: 5.8.3 vite: specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + version: 7.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@1.21.7)(jsdom@27.2.0)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) apps/desktop-e2e: devDependencies: @@ -1350,6 +1350,12 @@ importers: specifier: ^2.9.0 version: 2.9.0 + plugins/network: + dependencies: + '@tauri-apps/api': + specifier: ^2.9.0 + version: 2.9.0 + plugins/notification: dependencies: '@tauri-apps/api': @@ -21140,13 +21146,13 @@ snapshots: - tsx - yaml - '@tanstack/react-router-devtools@1.139.3(@tanstack/react-router@1.139.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.3)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10)(tsx@4.20.6)(yaml@2.8.1)': + '@tanstack/react-router-devtools@1.139.3(@tanstack/react-router@1.139.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(@tanstack/router-core@1.139.3)(@types/node@24.10.1)(csstype@3.2.3)(jiti@1.21.7)(lightningcss@1.30.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(solid-js@1.9.10)(tsx@4.20.6)(yaml@2.8.1)': dependencies: '@tanstack/react-router': 1.139.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - '@tanstack/router-devtools-core': 1.139.3(@tanstack/router-core@1.139.3)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.10)(tsx@4.20.6)(yaml@2.8.1) + '@tanstack/router-devtools-core': 1.139.3(@tanstack/router-core@1.139.3)(@types/node@24.10.1)(csstype@3.2.3)(jiti@1.21.7)(lightningcss@1.30.2)(solid-js@1.9.10)(tsx@4.20.6)(yaml@2.8.1) react: 19.2.0 react-dom: 19.2.0(react@19.2.0) - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) optionalDependencies: '@tanstack/router-core': 1.139.3 transitivePeerDependencies: @@ -21275,14 +21281,14 @@ snapshots: - tsx - yaml - '@tanstack/router-devtools-core@1.139.3(@tanstack/router-core@1.139.3)(@types/node@24.10.1)(csstype@3.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(solid-js@1.9.10)(tsx@4.20.6)(yaml@2.8.1)': + '@tanstack/router-devtools-core@1.139.3(@tanstack/router-core@1.139.3)(@types/node@24.10.1)(csstype@3.2.3)(jiti@1.21.7)(lightningcss@1.30.2)(solid-js@1.9.10)(tsx@4.20.6)(yaml@2.8.1)': dependencies: '@tanstack/router-core': 1.139.3 clsx: 2.1.1 goober: 2.1.18(csstype@3.2.3) solid-js: 1.9.10 tiny-invariant: 1.3.3 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) optionalDependencies: csstype: 3.2.3 transitivePeerDependencies: @@ -21333,7 +21339,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-plugin@1.139.3(@tanstack/react-router@1.139.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': + '@tanstack/router-plugin@1.139.3(@tanstack/react-router@1.139.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(vite@7.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.5) @@ -21351,7 +21357,7 @@ snapshots: zod: 3.25.76 optionalDependencies: '@tanstack/react-router': 1.139.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0) - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -22361,7 +22367,7 @@ snapshots: '@vercel/oidc@3.0.5': {} - '@vitejs/plugin-react@4.7.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': + '@vitejs/plugin-react@4.7.0(vite@7.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@babel/core': 7.28.5 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) @@ -22369,7 +22375,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + vite: 7.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -22416,6 +22422,14 @@ snapshots: optionalDependencies: vite: 7.2.4(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + '@vitest/mocker@3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + '@vitest/mocker@3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 @@ -31867,6 +31881,27 @@ snapshots: - tsx - yaml + vite-node@3.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@10.2.2) + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-node@3.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1): dependencies: cac: 6.7.14 @@ -31941,6 +31976,22 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 + vite@7.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.25.11 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.1 + fsevents: 2.3.3 + jiti: 1.21.7 + lightningcss: 1.30.2 + tsx: 4.20.6 + yaml: 2.8.1 + vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.11 @@ -32040,6 +32091,49 @@ snapshots: - tsx - yaml + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@1.21.7)(jsdom@27.2.0)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3(supports-color@10.2.2) + expect-type: 1.2.2 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 24.10.1 + jsdom: 27.2.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(jiti@2.6.1)(jsdom@27.2.0)(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1): dependencies: '@types/chai': 5.2.3