diff --git a/.claude/settings.local.json b/.claude/settings.local.json index f3bab6be5..ec121086c 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -8,7 +8,10 @@ "Bash(cargo build:*)", "Bash(npm run check:*)", "Bash(npm run:*)", - "Bash(tree:*)" + "Bash(tree:*)", + "Bash(find:*)", + "Bash(cargo check:*)", + "Bash(rm:*)" ], "deny": [], "ask": [] diff --git a/package.json b/package.json index 920c25313..f4bc80c7e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "proxycast", "private": true, - "version": "0.4.3", + "version": "0.5.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f4c151613..a2f8f8a85 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -121,6 +121,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto-launch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" +dependencies = [ + "dirs 4.0.0", + "thiserror 1.0.69", + "winreg 0.10.1", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -638,6 +649,15 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "5.0.1" @@ -656,6 +676,17 @@ dependencies = [ "dirs-sys 0.5.0", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + [[package]] name = "dirs-sys" version = "0.4.1" @@ -777,7 +808,7 @@ dependencies = [ "rustc_version", "toml 0.9.8", "vswhom", - "winreg", + "winreg 0.55.0", ] [[package]] @@ -2842,7 +2873,7 @@ dependencies = [ [[package]] name = "proxycast" -version = "0.4.3" +version = "0.5.0" dependencies = [ "async-stream", "axum", @@ -2857,6 +2888,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-autostart", "tauri-plugin-shell", "tokio", "tracing", @@ -3974,6 +4006,20 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-autostart" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459383cebc193cdd03d1ba4acc40f2c408a7abce419d64bdcd2d745bc2886f70" +dependencies = [ + "auto-launch", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", +] + [[package]] name = "tauri-plugin-shell" version = "2.3.3" @@ -5398,6 +5444,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winreg" version = "0.55.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 704e3f2f2..efd3ffa1f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxycast" -version = "0.4.3" +version = "0.5.0" description = "AI API Proxy Desktop App" authors = ["you"] edition = "2021" @@ -15,6 +15,7 @@ tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = [] } tauri-plugin-shell = "2" +tauri-plugin-autostart = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] } diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json index 86cdb1f5f..69ed83adf 100644 --- a/src-tauri/gen/schemas/acl-manifests.json +++ b/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"}},"required":["cmd","name"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["name","sidecar"],"type":"object"}],"definitions":{"ShellScopeEntryAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellScopeEntryAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellScopeEntryAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"Shell scope entry.","title":"ShellScopeEntry"}}} \ No newline at end of file +{"autostart":{"default_permission":{"identifier":"default","description":"This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n","permissions":["allow-enable","allow-disable","allow-is-enabled"]},"permissions":{"allow-disable":{"identifier":"allow-disable","description":"Enables the disable command without any pre-configured scope.","commands":{"allow":["disable"],"deny":[]}},"allow-enable":{"identifier":"allow-enable","description":"Enables the enable command without any pre-configured scope.","commands":{"allow":["enable"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"deny-disable":{"identifier":"deny-disable","description":"Denies the disable command without any pre-configured scope.","commands":{"allow":[],"deny":["disable"]}},"deny-enable":{"identifier":"deny-enable","description":"Denies the enable command without any pre-configured scope.","commands":{"allow":[],"deny":["enable"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"shell":{"default_permission":{"identifier":"default","description":"This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n","permissions":["allow-open"]},"permissions":{"allow-execute":{"identifier":"allow-execute","description":"Enables the execute command without any pre-configured scope.","commands":{"allow":["execute"],"deny":[]}},"allow-kill":{"identifier":"allow-kill","description":"Enables the kill command without any pre-configured scope.","commands":{"allow":["kill"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-spawn":{"identifier":"allow-spawn","description":"Enables the spawn command without any pre-configured scope.","commands":{"allow":["spawn"],"deny":[]}},"allow-stdin-write":{"identifier":"allow-stdin-write","description":"Enables the stdin_write command without any pre-configured scope.","commands":{"allow":["stdin_write"],"deny":[]}},"deny-execute":{"identifier":"deny-execute","description":"Denies the execute command without any pre-configured scope.","commands":{"allow":[],"deny":["execute"]}},"deny-kill":{"identifier":"deny-kill","description":"Denies the kill command without any pre-configured scope.","commands":{"allow":[],"deny":["kill"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-spawn":{"identifier":"deny-spawn","description":"Denies the spawn command without any pre-configured scope.","commands":{"allow":[],"deny":["spawn"]}},"deny-stdin-write":{"identifier":"deny-stdin-write","description":"Denies the stdin_write command without any pre-configured scope.","commands":{"allow":[],"deny":["stdin_write"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"cmd":{"description":"The command name. It can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"}},"required":["cmd","name"],"type":"object"},{"additionalProperties":false,"properties":{"args":{"allOf":[{"$ref":"#/definitions/ShellScopeEntryAllowedArgs"}],"description":"The allowed arguments for the command execution."},"name":{"description":"The name for this allowed shell command configuration.\n\nThis name will be used inside of the webview API to call this command along with any specified arguments.","type":"string"},"sidecar":{"description":"If this command is a sidecar command.","type":"boolean"}},"required":["name","sidecar"],"type":"object"}],"definitions":{"ShellScopeEntryAllowedArg":{"anyOf":[{"description":"A non-configurable argument that is passed to the command in the order it was specified.","type":"string"},{"additionalProperties":false,"description":"A variable that is set while calling the command from the webview API.","properties":{"raw":{"default":false,"description":"Marks the validator as a raw regex, meaning the plugin should not make any modification at runtime.\n\nThis means the regex will not match on the entire string by default, which might be exploited if your regex allow unexpected input to be considered valid. When using this option, make sure your regex is correct.","type":"boolean"},"validator":{"description":"[regex] validator to require passed values to conform to an expected input.\n\nThis will require the argument value passed to this variable to match the `validator` regex before it will be executed.\n\nThe regex string is by default surrounded by `^...$` to match the full string. For example the `https?://\\w+` regex would be registered as `^https?://\\w+$`.\n\n[regex]: ","type":"string"}},"required":["validator"],"type":"object"}],"description":"A command argument allowed to be executed by the webview API."},"ShellScopeEntryAllowedArgs":{"anyOf":[{"description":"Use a simple boolean to allow all or disable all arguments to this command configuration.","type":"boolean"},{"description":"A specific set of [`ShellScopeEntryAllowedArg`] that are valid to call for the command configuration.","items":{"$ref":"#/definitions/ShellScopeEntryAllowedArg"},"type":"array"}],"description":"A set of command arguments allowed to be executed by the webview API.\n\nA value of `true` will allow any arguments to be passed to the command. `false` will disable all arguments. A list of [`ShellScopeEntryAllowedArg`] will set those arguments as the only valid arguments to be passed to the attached command configuration."}},"description":"Shell scope entry.","title":"ShellScopeEntry"}}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/desktop-schema.json b/src-tauri/gen/schemas/desktop-schema.json index f827fe175..292903497 100644 --- a/src-tauri/gen/schemas/desktop-schema.json +++ b/src-tauri/gen/schemas/desktop-schema.json @@ -386,6 +386,48 @@ "Identifier": { "description": "Permission identifier", "oneOf": [ + { + "description": "This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n\n#### This default permission set includes:\n\n- `allow-enable`\n- `allow-disable`\n- `allow-is-enabled`", + "type": "string", + "const": "autostart:default", + "markdownDescription": "This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n\n#### This default permission set includes:\n\n- `allow-enable`\n- `allow-disable`\n- `allow-is-enabled`" + }, + { + "description": "Enables the disable command without any pre-configured scope.", + "type": "string", + "const": "autostart:allow-disable", + "markdownDescription": "Enables the disable command without any pre-configured scope." + }, + { + "description": "Enables the enable command without any pre-configured scope.", + "type": "string", + "const": "autostart:allow-enable", + "markdownDescription": "Enables the enable command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "autostart:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the disable command without any pre-configured scope.", + "type": "string", + "const": "autostart:deny-disable", + "markdownDescription": "Denies the disable command without any pre-configured scope." + }, + { + "description": "Denies the enable command without any pre-configured scope.", + "type": "string", + "const": "autostart:deny-enable", + "markdownDescription": "Denies the enable command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "autostart:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, { "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", "type": "string", diff --git a/src-tauri/gen/schemas/macOS-schema.json b/src-tauri/gen/schemas/macOS-schema.json index f827fe175..292903497 100644 --- a/src-tauri/gen/schemas/macOS-schema.json +++ b/src-tauri/gen/schemas/macOS-schema.json @@ -386,6 +386,48 @@ "Identifier": { "description": "Permission identifier", "oneOf": [ + { + "description": "This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n\n#### This default permission set includes:\n\n- `allow-enable`\n- `allow-disable`\n- `allow-is-enabled`", + "type": "string", + "const": "autostart:default", + "markdownDescription": "This permission set configures if your\napplication can enable or disable auto\nstarting the application on boot.\n\n#### Granted Permissions\n\nIt allows all to check, enable and\ndisable the automatic start on boot.\n\n\n#### This default permission set includes:\n\n- `allow-enable`\n- `allow-disable`\n- `allow-is-enabled`" + }, + { + "description": "Enables the disable command without any pre-configured scope.", + "type": "string", + "const": "autostart:allow-disable", + "markdownDescription": "Enables the disable command without any pre-configured scope." + }, + { + "description": "Enables the enable command without any pre-configured scope.", + "type": "string", + "const": "autostart:allow-enable", + "markdownDescription": "Enables the enable command without any pre-configured scope." + }, + { + "description": "Enables the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "autostart:allow-is-enabled", + "markdownDescription": "Enables the is_enabled command without any pre-configured scope." + }, + { + "description": "Denies the disable command without any pre-configured scope.", + "type": "string", + "const": "autostart:deny-disable", + "markdownDescription": "Denies the disable command without any pre-configured scope." + }, + { + "description": "Denies the enable command without any pre-configured scope.", + "type": "string", + "const": "autostart:deny-enable", + "markdownDescription": "Denies the enable command without any pre-configured scope." + }, + { + "description": "Denies the is_enabled command without any pre-configured scope.", + "type": "string", + "const": "autostart:deny-is-enabled", + "markdownDescription": "Denies the is_enabled command without any pre-configured scope." + }, { "description": "Default core plugins set.\n#### This default permission set includes:\n\n- `core:path:default`\n- `core:event:default`\n- `core:window:default`\n- `core:webview:default`\n- `core:app:default`\n- `core:image:default`\n- `core:resources:default`\n- `core:menu:default`\n- `core:tray:default`", "type": "string", diff --git a/src-tauri/src/commands/config_cmd.rs b/src-tauri/src/commands/config_cmd.rs new file mode 100644 index 000000000..91b825cfa --- /dev/null +++ b/src-tauri/src/commands/config_cmd.rs @@ -0,0 +1,196 @@ +use crate::models::AppType; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use tauri::AppHandle; +use tauri_plugin_autostart::ManagerExt; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConfigStatus { + pub exists: bool, + pub path: String, + pub has_env: bool, +} + +/// Get the config directory path for an app type +fn get_config_dir(app_type: &AppType) -> Option { + let home = dirs::home_dir()?; + match app_type { + AppType::Claude => Some(home.join(".claude")), + AppType::Codex => Some(home.join(".codex")), + AppType::Gemini => Some(home.join(".gemini")), + AppType::ProxyCast => dirs::config_dir().map(|d| d.join("proxycast")), + } +} + +#[tauri::command] +pub fn get_config_status(app_type: String) -> Result { + let app = app_type.parse::().map_err(|e| e.to_string())?; + let config_dir = get_config_dir(&app).ok_or("Cannot determine config directory")?; + + let main_config = match app { + AppType::Claude => config_dir.join("settings.json"), + AppType::Codex => config_dir.join("auth.json"), + AppType::Gemini => config_dir.join(".env"), + AppType::ProxyCast => config_dir.join("config.json"), + }; + + let has_env = match app { + AppType::Claude => { + config_dir.join("settings.json").exists() + && std::fs::read_to_string(config_dir.join("settings.json")) + .map(|s| s.contains("env")) + .unwrap_or(false) + } + AppType::Codex => config_dir.join("auth.json").exists(), + AppType::Gemini => config_dir.join(".env").exists(), + AppType::ProxyCast => config_dir.join("config.json").exists(), + }; + + Ok(ConfigStatus { + exists: main_config.exists(), + path: config_dir.to_string_lossy().to_string(), + has_env, + }) +} + +#[tauri::command] +pub fn get_config_dir_path(app_type: String) -> Result { + let app = app_type.parse::().map_err(|e| e.to_string())?; + let config_dir = get_config_dir(&app).ok_or("Cannot determine config directory")?; + Ok(config_dir.to_string_lossy().to_string()) +} + +#[tauri::command] +pub async fn open_config_folder(handle: AppHandle, app_type: String) -> Result { + let app = app_type.parse::().map_err(|e| e.to_string())?; + let config_dir = get_config_dir(&app).ok_or("Cannot determine config directory")?; + + if !config_dir.exists() { + std::fs::create_dir_all(&config_dir).map_err(|e| e.to_string())?; + } + + #[cfg(target_os = "macos")] + { + std::process::Command::new("open") + .arg(&config_dir) + .spawn() + .map_err(|e| e.to_string())?; + } + + #[cfg(target_os = "windows")] + { + std::process::Command::new("explorer") + .arg(&config_dir) + .spawn() + .map_err(|e| e.to_string())?; + } + + #[cfg(target_os = "linux")] + { + std::process::Command::new("xdg-open") + .arg(&config_dir) + .spawn() + .map_err(|e| e.to_string())?; + } + + Ok(true) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ToolVersion { + pub name: String, + pub version: Option, + pub installed: bool, +} + +#[tauri::command] +pub async fn get_tool_versions() -> Result, String> { + let mut versions = Vec::new(); + + // Check Claude Code version + let claude_version = std::process::Command::new("claude") + .arg("--version") + .output() + .ok() + .and_then(|o| { + if o.status.success() { + String::from_utf8(o.stdout).ok() + } else { + None + } + }) + .map(|s| s.trim().to_string()); + + versions.push(ToolVersion { + name: "Claude Code".to_string(), + version: claude_version.clone(), + installed: claude_version.is_some(), + }); + + // Check Codex version + let codex_version = std::process::Command::new("codex") + .arg("--version") + .output() + .ok() + .and_then(|o| { + if o.status.success() { + String::from_utf8(o.stdout).ok() + } else { + None + } + }) + .map(|s| s.trim().to_string()); + + versions.push(ToolVersion { + name: "Codex".to_string(), + version: codex_version.clone(), + installed: codex_version.is_some(), + }); + + // Check Gemini CLI version + let gemini_version = std::process::Command::new("gemini") + .arg("--version") + .output() + .ok() + .and_then(|o| { + if o.status.success() { + String::from_utf8(o.stdout).ok() + } else { + None + } + }) + .map(|s| s.trim().to_string()); + + versions.push(ToolVersion { + name: "Gemini CLI".to_string(), + version: gemini_version.clone(), + installed: gemini_version.is_some(), + }); + + Ok(versions) +} + +#[tauri::command] +pub async fn get_auto_launch_status(app: AppHandle) -> Result { + let autostart_manager = app.autolaunch(); + autostart_manager + .is_enabled() + .map_err(|e| format!("Failed to get autostart status: {}", e)) +} + +#[tauri::command] +pub async fn set_auto_launch(app: AppHandle, enabled: bool) -> Result { + let autostart_manager = app.autolaunch(); + + if enabled { + autostart_manager + .enable() + .map_err(|e| format!("Failed to enable autostart: {}", e))?; + } else { + autostart_manager + .disable() + .map_err(|e| format!("Failed to disable autostart: {}", e))?; + } + + Ok(enabled) +} diff --git a/src-tauri/src/commands/mcp_cmd.rs b/src-tauri/src/commands/mcp_cmd.rs new file mode 100644 index 000000000..f71c69236 --- /dev/null +++ b/src-tauri/src/commands/mcp_cmd.rs @@ -0,0 +1,44 @@ +use crate::database::DbConnection; +use crate::models::McpServer; +use crate::services::mcp_service::McpService; +use tauri::State; + +#[tauri::command] +pub fn get_mcp_servers(db: State<'_, DbConnection>) -> Result, String> { + McpService::get_all(&db) +} + +#[tauri::command] +pub fn add_mcp_server(db: State<'_, DbConnection>, server: McpServer) -> Result<(), String> { + McpService::add(&db, server) +} + +#[tauri::command] +pub fn update_mcp_server(db: State<'_, DbConnection>, server: McpServer) -> Result<(), String> { + McpService::update(&db, server) +} + +#[tauri::command] +pub fn delete_mcp_server(db: State<'_, DbConnection>, id: String) -> Result<(), String> { + McpService::delete(&db, &id) +} + +#[tauri::command] +pub fn toggle_mcp_server( + db: State<'_, DbConnection>, + id: String, + app_type: String, + enabled: bool, +) -> Result<(), String> { + McpService::toggle_enabled(&db, &id, &app_type, enabled) +} + +#[tauri::command] +pub fn import_mcp_from_app(db: State<'_, DbConnection>, app_type: String) -> Result { + McpService::import_from_app(&db, &app_type) +} + +#[tauri::command] +pub fn sync_all_mcp_to_live(db: State<'_, DbConnection>) -> Result<(), String> { + McpService::sync_all_to_live(&db) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs new file mode 100644 index 000000000..cc1321d8a --- /dev/null +++ b/src-tauri/src/commands/mod.rs @@ -0,0 +1,5 @@ +pub mod config_cmd; +pub mod mcp_cmd; +pub mod oauth_cmd; +pub mod prompt_cmd; +pub mod switch_cmd; diff --git a/src-tauri/src/commands/oauth_cmd.rs b/src-tauri/src/commands/oauth_cmd.rs new file mode 100644 index 000000000..b9e9f3dc4 --- /dev/null +++ b/src-tauri/src/commands/oauth_cmd.rs @@ -0,0 +1,488 @@ +//! Unified OAuth Commands for Kiro/Gemini/Qwen Providers +//! +//! This module consolidates the OAuth credential management commands +//! for all three OAuth providers into a single set of parameterized commands. + +use crate::providers; +use crate::AppState; +use crate::LogState; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use tauri::State; + +/// Supported OAuth provider types +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum OAuthProvider { + Kiro, + Gemini, + Qwen, +} + +impl OAuthProvider { + pub fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "kiro" => Ok(OAuthProvider::Kiro), + "gemini" => Ok(OAuthProvider::Gemini), + "qwen" => Ok(OAuthProvider::Qwen), + _ => Err(format!("Unknown provider: {}", s)), + } + } + + pub fn display_name(&self) -> &'static str { + match self { + OAuthProvider::Kiro => "Kiro", + OAuthProvider::Gemini => "Gemini", + OAuthProvider::Qwen => "Qwen", + } + } +} + +/// Unified credential status for all OAuth providers +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OAuthCredentialStatus { + pub provider: String, + pub loaded: bool, + pub has_access_token: bool, + pub has_refresh_token: bool, + pub is_valid: bool, + pub expiry_info: Option, + pub creds_path: String, + /// Provider-specific additional info + pub extra: serde_json::Value, +} + +/// Environment variable representation +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EnvVariable { + pub key: String, + pub value: String, + pub masked: String, +} + +/// Result of credential file change check +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CheckResult { + pub changed: bool, + pub new_hash: String, + pub reloaded: bool, +} + +fn mask_token(token: &str) -> String { + let chars: Vec = token.chars().collect(); + if chars.len() <= 12 { + "****".to_string() + } else { + let prefix: String = chars[..6].iter().collect(); + let suffix: String = chars[chars.len() - 4..].iter().collect(); + format!("{prefix}****{suffix}") + } +} + +fn get_creds_path(provider: &OAuthProvider) -> PathBuf { + match provider { + OAuthProvider::Kiro => providers::kiro::KiroProvider::default_creds_path(), + OAuthProvider::Gemini => providers::gemini::GeminiProvider::default_creds_path(), + OAuthProvider::Qwen => providers::qwen::QwenProvider::default_creds_path(), + } +} + +/// Get OAuth credentials status for a provider +#[tauri::command] +pub async fn get_oauth_credentials( + state: State<'_, AppState>, + provider: String, +) -> Result { + let provider_type = OAuthProvider::from_str(&provider)?; + let s = state.read().await; + let path = get_creds_path(&provider_type); + + match provider_type { + OAuthProvider::Kiro => { + let creds = &s.kiro_provider.credentials; + Ok(OAuthCredentialStatus { + provider: provider.clone(), + loaded: creds.access_token.is_some() || creds.refresh_token.is_some(), + has_access_token: creds.access_token.is_some(), + has_refresh_token: creds.refresh_token.is_some(), + is_valid: creds.access_token.is_some() && !s.kiro_provider.is_token_expiring_soon(), + expiry_info: creds.expires_at.clone(), + creds_path: path.to_string_lossy().to_string(), + extra: serde_json::json!({ + "region": creds.region, + "auth_method": creds.auth_method, + }), + }) + } + OAuthProvider::Gemini => { + let creds = &s.gemini_provider.credentials; + Ok(OAuthCredentialStatus { + provider: provider.clone(), + loaded: creds.access_token.is_some() || creds.refresh_token.is_some(), + has_access_token: creds.access_token.is_some(), + has_refresh_token: creds.refresh_token.is_some(), + is_valid: s.gemini_provider.is_token_valid(), + expiry_info: creds.expiry_date.map(|d| d.to_string()), + creds_path: path.to_string_lossy().to_string(), + extra: serde_json::json!({}), + }) + } + OAuthProvider::Qwen => { + let creds = &s.qwen_provider.credentials; + Ok(OAuthCredentialStatus { + provider: provider.clone(), + loaded: creds.access_token.is_some() || creds.refresh_token.is_some(), + has_access_token: creds.access_token.is_some(), + has_refresh_token: creds.refresh_token.is_some(), + is_valid: s.qwen_provider.is_token_valid(), + expiry_info: creds.expiry_date.map(|d| d.to_string()), + creds_path: path.to_string_lossy().to_string(), + extra: serde_json::json!({ + "resource_url": creds.resource_url, + }), + }) + } + } +} + +/// Reload OAuth credentials from file +#[tauri::command] +pub async fn reload_oauth_credentials( + state: State<'_, AppState>, + logs: State<'_, LogState>, + provider: String, +) -> Result { + let provider_type = OAuthProvider::from_str(&provider)?; + let display_name = provider_type.display_name(); + + logs.write() + .await + .add("info", &format!("[{display_name}] 正在加载凭证...")); + + let mut s = state.write().await; + + let result = match provider_type { + OAuthProvider::Kiro => s.kiro_provider.load_credentials().await, + OAuthProvider::Gemini => s.gemini_provider.load_credentials().await, + OAuthProvider::Qwen => s.qwen_provider.load_credentials().await, + }; + + match result { + Ok(_) => { + logs.write() + .await + .add("info", &format!("[{display_name}] 凭证加载成功")); + Ok(format!("{display_name} credentials reloaded")) + } + Err(e) => { + logs.write() + .await + .add("error", &format!("[{display_name}] 凭证加载失败: {e}")); + Err(e.to_string()) + } + } +} + +/// Refresh OAuth token for a provider +#[tauri::command] +pub async fn refresh_oauth_token( + state: State<'_, AppState>, + logs: State<'_, LogState>, + provider: String, +) -> Result { + let provider_type = OAuthProvider::from_str(&provider)?; + let display_name = provider_type.display_name(); + + logs.write() + .await + .add("info", &format!("[{display_name}] 正在刷新 Token...")); + + let mut s = state.write().await; + + let result = match provider_type { + OAuthProvider::Kiro => s.kiro_provider.refresh_token().await, + OAuthProvider::Gemini => s.gemini_provider.refresh_token().await, + OAuthProvider::Qwen => s.qwen_provider.refresh_token().await, + }; + + match result { + Ok(token) => { + logs.write() + .await + .add("info", &format!("[{display_name}] Token 刷新成功")); + Ok(token) + } + Err(e) => { + logs.write() + .await + .add("error", &format!("[{display_name}] Token 刷新失败: {e}")); + Err(e.to_string()) + } + } +} + +/// Get environment variables for a provider +#[tauri::command] +pub async fn get_oauth_env_variables( + state: State<'_, AppState>, + provider: String, +) -> Result, String> { + let provider_type = OAuthProvider::from_str(&provider)?; + let s = state.read().await; + let mut vars = Vec::new(); + + match provider_type { + OAuthProvider::Kiro => { + let creds = &s.kiro_provider.credentials; + if let Some(token) = &creds.access_token { + vars.push(EnvVariable { + key: "KIRO_ACCESS_TOKEN".to_string(), + value: token.clone(), + masked: mask_token(token), + }); + } + if let Some(token) = &creds.refresh_token { + vars.push(EnvVariable { + key: "KIRO_REFRESH_TOKEN".to_string(), + value: token.clone(), + masked: mask_token(token), + }); + } + if let Some(id) = &creds.client_id { + vars.push(EnvVariable { + key: "KIRO_CLIENT_ID".to_string(), + value: id.clone(), + masked: mask_token(id), + }); + } + if let Some(secret) = &creds.client_secret { + vars.push(EnvVariable { + key: "KIRO_CLIENT_SECRET".to_string(), + value: secret.clone(), + masked: mask_token(secret), + }); + } + if let Some(arn) = &creds.profile_arn { + vars.push(EnvVariable { + key: "KIRO_PROFILE_ARN".to_string(), + value: arn.clone(), + masked: arn.clone(), + }); + } + if let Some(region) = &creds.region { + vars.push(EnvVariable { + key: "KIRO_REGION".to_string(), + value: region.clone(), + masked: region.clone(), + }); + } + if let Some(method) = &creds.auth_method { + vars.push(EnvVariable { + key: "KIRO_AUTH_METHOD".to_string(), + value: method.clone(), + masked: method.clone(), + }); + } + } + OAuthProvider::Gemini => { + let creds = &s.gemini_provider.credentials; + if let Some(token) = &creds.access_token { + vars.push(EnvVariable { + key: "GEMINI_ACCESS_TOKEN".to_string(), + value: token.clone(), + masked: mask_token(token), + }); + } + if let Some(token) = &creds.refresh_token { + vars.push(EnvVariable { + key: "GEMINI_REFRESH_TOKEN".to_string(), + value: token.clone(), + masked: mask_token(token), + }); + } + if let Some(expiry) = creds.expiry_date { + let expiry_str = expiry.to_string(); + vars.push(EnvVariable { + key: "GEMINI_EXPIRY_DATE".to_string(), + value: expiry_str.clone(), + masked: expiry_str, + }); + } + } + OAuthProvider::Qwen => { + let creds = &s.qwen_provider.credentials; + if let Some(token) = &creds.access_token { + vars.push(EnvVariable { + key: "QWEN_ACCESS_TOKEN".to_string(), + value: token.clone(), + masked: mask_token(token), + }); + } + if let Some(token) = &creds.refresh_token { + vars.push(EnvVariable { + key: "QWEN_REFRESH_TOKEN".to_string(), + value: token.clone(), + masked: mask_token(token), + }); + } + if let Some(url) = &creds.resource_url { + vars.push(EnvVariable { + key: "QWEN_RESOURCE_URL".to_string(), + value: url.clone(), + masked: url.clone(), + }); + } + if let Some(expiry) = creds.expiry_date { + let expiry_str = expiry.to_string(); + vars.push(EnvVariable { + key: "QWEN_EXPIRY_DATE".to_string(), + value: expiry_str.clone(), + masked: expiry_str, + }); + } + } + } + + Ok(vars) +} + +/// Get token file hash for a provider +#[tauri::command] +pub async fn get_oauth_token_file_hash(provider: String) -> Result { + let provider_type = OAuthProvider::from_str(&provider)?; + let path = get_creds_path(&provider_type); + + if !tokio::fs::try_exists(&path).await.unwrap_or(false) { + return Ok("".to_string()); + } + + let content = tokio::fs::read(&path).await.map_err(|e| e.to_string())?; + let hash = format!("{:x}", md5::compute(&content)); + Ok(hash) +} + +/// Check credential file changes and auto-reload +#[tauri::command] +pub async fn check_and_reload_oauth_credentials( + state: State<'_, AppState>, + logs: State<'_, LogState>, + provider: String, + last_hash: String, +) -> Result { + let provider_type = OAuthProvider::from_str(&provider)?; + let display_name = provider_type.display_name(); + let path = get_creds_path(&provider_type); + + if !tokio::fs::try_exists(&path).await.unwrap_or(false) { + return Ok(CheckResult { + changed: false, + new_hash: "".to_string(), + reloaded: false, + }); + } + + let content = tokio::fs::read(&path).await.map_err(|e| e.to_string())?; + let new_hash = format!("{:x}", md5::compute(&content)); + + if !last_hash.is_empty() && new_hash != last_hash { + logs.write().await.add( + "info", + &format!("[{display_name}][自动检测] 凭证文件已变化,正在重新加载..."), + ); + + let mut s = state.write().await; + let result = match provider_type { + OAuthProvider::Kiro => s.kiro_provider.load_credentials().await, + OAuthProvider::Gemini => s.gemini_provider.load_credentials().await, + OAuthProvider::Qwen => s.qwen_provider.load_credentials().await, + }; + + match result { + Ok(_) => { + logs.write() + .await + .add("info", &format!("[{display_name}][自动检测] 凭证重新加载成功")); + Ok(CheckResult { + changed: true, + new_hash, + reloaded: true, + }) + } + Err(e) => { + logs.write().await.add( + "error", + &format!("[{display_name}][自动检测] 凭证重新加载失败: {e}"), + ); + Ok(CheckResult { + changed: true, + new_hash, + reloaded: false, + }) + } + } + } else { + Ok(CheckResult { + changed: false, + new_hash, + reloaded: false, + }) + } +} + +/// Get all OAuth providers status at once +#[tauri::command] +pub async fn get_all_oauth_credentials( + state: State<'_, AppState>, +) -> Result, String> { + let s = state.read().await; + let mut results = Vec::new(); + + // Kiro + let kiro_creds = &s.kiro_provider.credentials; + let kiro_path = providers::kiro::KiroProvider::default_creds_path(); + results.push(OAuthCredentialStatus { + provider: "kiro".to_string(), + loaded: kiro_creds.access_token.is_some() || kiro_creds.refresh_token.is_some(), + has_access_token: kiro_creds.access_token.is_some(), + has_refresh_token: kiro_creds.refresh_token.is_some(), + is_valid: kiro_creds.access_token.is_some() && !s.kiro_provider.is_token_expiring_soon(), + expiry_info: kiro_creds.expires_at.clone(), + creds_path: kiro_path.to_string_lossy().to_string(), + extra: serde_json::json!({ + "region": kiro_creds.region, + "auth_method": kiro_creds.auth_method, + }), + }); + + // Gemini + let gemini_creds = &s.gemini_provider.credentials; + let gemini_path = providers::gemini::GeminiProvider::default_creds_path(); + results.push(OAuthCredentialStatus { + provider: "gemini".to_string(), + loaded: gemini_creds.access_token.is_some() || gemini_creds.refresh_token.is_some(), + has_access_token: gemini_creds.access_token.is_some(), + has_refresh_token: gemini_creds.refresh_token.is_some(), + is_valid: s.gemini_provider.is_token_valid(), + expiry_info: gemini_creds.expiry_date.map(|d| d.to_string()), + creds_path: gemini_path.to_string_lossy().to_string(), + extra: serde_json::json!({}), + }); + + // Qwen + let qwen_creds = &s.qwen_provider.credentials; + let qwen_path = providers::qwen::QwenProvider::default_creds_path(); + results.push(OAuthCredentialStatus { + provider: "qwen".to_string(), + loaded: qwen_creds.access_token.is_some() || qwen_creds.refresh_token.is_some(), + has_access_token: qwen_creds.access_token.is_some(), + has_refresh_token: qwen_creds.refresh_token.is_some(), + is_valid: s.qwen_provider.is_token_valid(), + expiry_info: qwen_creds.expiry_date.map(|d| d.to_string()), + creds_path: qwen_path.to_string_lossy().to_string(), + extra: serde_json::json!({ + "resource_url": qwen_creds.resource_url, + }), + }); + + Ok(results) +} diff --git a/src-tauri/src/commands/prompt_cmd.rs b/src-tauri/src/commands/prompt_cmd.rs new file mode 100644 index 000000000..a256c90e6 --- /dev/null +++ b/src-tauri/src/commands/prompt_cmd.rs @@ -0,0 +1,92 @@ +use crate::database::DbConnection; +use crate::models::Prompt; +use crate::services::prompt_service::PromptService; +use std::collections::HashMap; +use tauri::State; + +/// Get all prompts for an app type (as HashMap for frontend) +#[tauri::command] +pub fn get_prompts( + db: State<'_, DbConnection>, + app: String, +) -> Result, String> { + PromptService::get_all_map(&db, &app) +} + +/// Upsert a prompt (insert or update) +#[tauri::command] +pub fn upsert_prompt( + db: State<'_, DbConnection>, + app: String, + id: String, + prompt: Prompt, +) -> Result<(), String> { + // Ensure the prompt has the correct app_type and id + let mut prompt = prompt; + prompt.app_type = app.clone(); + prompt.id = id; + PromptService::upsert(&db, &app, prompt) +} + +/// Add a new prompt +#[tauri::command] +pub fn add_prompt(db: State<'_, DbConnection>, prompt: Prompt) -> Result<(), String> { + PromptService::add(&db, prompt) +} + +/// Update an existing prompt +#[tauri::command] +pub fn update_prompt(db: State<'_, DbConnection>, prompt: Prompt) -> Result<(), String> { + PromptService::update(&db, prompt) +} + +/// Delete a prompt +#[tauri::command] +pub fn delete_prompt( + db: State<'_, DbConnection>, + app: String, + id: String, +) -> Result<(), String> { + PromptService::delete(&db, &app, &id) +} + +/// Enable a prompt and sync to live file +#[tauri::command] +pub fn enable_prompt( + db: State<'_, DbConnection>, + app: String, + id: String, +) -> Result<(), String> { + PromptService::enable(&db, &app, &id) +} + +/// Import prompt from live file +#[tauri::command] +pub fn import_prompt_from_file( + db: State<'_, DbConnection>, + app: String, +) -> Result { + PromptService::import_from_file(&db, &app) +} + +/// Get current live prompt file content +#[tauri::command] +pub fn get_current_prompt_file_content(app: String) -> Result, String> { + PromptService::get_live_content(&app) +} + +/// Auto-import prompt from live file on first launch (if no prompts exist) +#[tauri::command] +pub fn auto_import_prompt(db: State<'_, DbConnection>, app: String) -> Result { + PromptService::import_on_first_launch(&db, &app) +} + +// Legacy command for compatibility +#[tauri::command] +pub fn switch_prompt( + db: State<'_, DbConnection>, + app_type: String, + id: String, +) -> Result<(), String> { + PromptService::enable(&db, &app_type, &id) +} diff --git a/src-tauri/src/commands/switch_cmd.rs b/src-tauri/src/commands/switch_cmd.rs new file mode 100644 index 000000000..7feef299a --- /dev/null +++ b/src-tauri/src/commands/switch_cmd.rs @@ -0,0 +1,65 @@ +use crate::database::DbConnection; +use crate::models::Provider; +use crate::services::switch::SwitchService; +use serde_json::Value; +use tauri::State; + +#[tauri::command] +pub fn get_switch_providers( + db: State<'_, DbConnection>, + app_type: String, +) -> Result, String> { + SwitchService::get_providers(&db, &app_type) +} + +#[tauri::command] +pub fn get_current_switch_provider( + db: State<'_, DbConnection>, + app_type: String, +) -> Result, String> { + SwitchService::get_current_provider(&db, &app_type) +} + +#[tauri::command] +pub fn add_switch_provider(db: State<'_, DbConnection>, provider: Provider) -> Result<(), String> { + SwitchService::add_provider(&db, provider) +} + +#[tauri::command] +pub fn update_switch_provider( + db: State<'_, DbConnection>, + provider: Provider, +) -> Result<(), String> { + SwitchService::update_provider(&db, provider) +} + +#[tauri::command] +pub fn delete_switch_provider( + db: State<'_, DbConnection>, + app_type: String, + id: String, +) -> Result<(), String> { + SwitchService::delete_provider(&db, &app_type, &id) +} + +#[tauri::command] +pub fn switch_provider( + db: State<'_, DbConnection>, + app_type: String, + id: String, +) -> Result<(), String> { + SwitchService::switch_provider(&db, &app_type, &id) +} + +#[tauri::command] +pub fn import_default_config( + db: State<'_, DbConnection>, + app_type: String, +) -> Result { + SwitchService::import_default_config(&db, &app_type) +} + +#[tauri::command] +pub fn read_live_provider_settings(app_type: String) -> Result { + SwitchService::read_live_settings(&app_type) +} diff --git a/src-tauri/src/database/dao/mcp.rs b/src-tauri/src/database/dao/mcp.rs index 9e3b37c9d..28d690263 100644 --- a/src-tauri/src/database/dao/mcp.rs +++ b/src-tauri/src/database/dao/mcp.rs @@ -1,2 +1,103 @@ -// MCP Servers DAO - 数据访问对象 -// TODO: 实现 mcp_servers 表的 CRUD 操作 +use crate::models::McpServer; +use rusqlite::{params, Connection}; +use serde_json::Value; + +pub struct McpDao; + +impl McpDao { + pub fn get_all(conn: &Connection) -> Result, rusqlite::Error> { + let mut stmt = conn.prepare( + "SELECT id, name, server_config, description, enabled_proxycast, + enabled_claude, enabled_codex, enabled_gemini, created_at + FROM mcp_servers ORDER BY created_at", + )?; + + let servers = stmt.query_map([], |row| { + let config_str: String = row.get(2)?; + let server_config: Value = serde_json::from_str(&config_str).unwrap_or(Value::Null); + + Ok(McpServer { + id: row.get(0)?, + name: row.get(1)?, + server_config, + description: row.get(3)?, + enabled_proxycast: row.get::<_, i32>(4)? == 1, + enabled_claude: row.get::<_, i32>(5)? == 1, + enabled_codex: row.get::<_, i32>(6)? == 1, + enabled_gemini: row.get::<_, i32>(7)? == 1, + created_at: row.get(8)?, + }) + })?; + + servers.collect() + } + + pub fn insert(conn: &Connection, server: &McpServer) -> Result<(), rusqlite::Error> { + conn.execute( + "INSERT INTO mcp_servers (id, name, server_config, description, + enabled_proxycast, enabled_claude, enabled_codex, + enabled_gemini, created_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)", + params![ + server.id, + server.name, + serde_json::to_string(&server.server_config).unwrap_or_default(), + server.description, + if server.enabled_proxycast { 1 } else { 0 }, + if server.enabled_claude { 1 } else { 0 }, + if server.enabled_codex { 1 } else { 0 }, + if server.enabled_gemini { 1 } else { 0 }, + server.created_at, + ], + )?; + Ok(()) + } + + pub fn update(conn: &Connection, server: &McpServer) -> Result<(), rusqlite::Error> { + conn.execute( + "UPDATE mcp_servers SET name = ?1, server_config = ?2, description = ?3, + enabled_proxycast = ?4, enabled_claude = ?5, enabled_codex = ?6, enabled_gemini = ?7 + WHERE id = ?8", + params![ + server.name, + serde_json::to_string(&server.server_config).unwrap_or_default(), + server.description, + if server.enabled_proxycast { 1 } else { 0 }, + if server.enabled_claude { 1 } else { 0 }, + if server.enabled_codex { 1 } else { 0 }, + if server.enabled_gemini { 1 } else { 0 }, + server.id, + ], + )?; + Ok(()) + } + + pub fn delete(conn: &Connection, id: &str) -> Result<(), rusqlite::Error> { + conn.execute("DELETE FROM mcp_servers WHERE id = ?", [id])?; + Ok(()) + } + + pub fn toggle_enabled( + conn: &Connection, + id: &str, + app_type: &str, + enabled: bool, + ) -> Result<(), rusqlite::Error> { + let column = match app_type { + "proxycast" => "enabled_proxycast", + "claude" => "enabled_claude", + "codex" => "enabled_codex", + "gemini" => "enabled_gemini", + _ => { + return Err(rusqlite::Error::InvalidParameterName(format!( + "Invalid app_type: {}", + app_type + ))) + } + }; + + let sql = format!("UPDATE mcp_servers SET {} = ? WHERE id = ?", column); + conn.execute(&sql, params![if enabled { 1 } else { 0 }, id])?; + Ok(()) + } +} diff --git a/src-tauri/src/database/dao/mod.rs b/src-tauri/src/database/dao/mod.rs index d489b7f38..810fde969 100644 --- a/src-tauri/src/database/dao/mod.rs +++ b/src-tauri/src/database/dao/mod.rs @@ -1,3 +1,7 @@ pub mod mcp; pub mod prompts; pub mod providers; + +pub use mcp::McpDao; +pub use prompts::PromptDao; +pub use providers::ProviderDao; diff --git a/src-tauri/src/database/dao/prompts.rs b/src-tauri/src/database/dao/prompts.rs index 5ab7dc09e..79b5288c2 100644 --- a/src-tauri/src/database/dao/prompts.rs +++ b/src-tauri/src/database/dao/prompts.rs @@ -1,2 +1,190 @@ -// Prompts DAO - 数据访问对象 -// TODO: 实现 prompts 表的 CRUD 操作 +use crate::models::Prompt; +use rusqlite::{params, Connection}; +use std::collections::HashMap; + +pub struct PromptDao; + +impl PromptDao { + /// Get all prompts for an app type + pub fn get_all(conn: &Connection, app_type: &str) -> Result, rusqlite::Error> { + let mut stmt = conn.prepare( + "SELECT id, app_type, name, content, description, enabled, created_at, updated_at + FROM prompts WHERE app_type = ? ORDER BY created_at", + )?; + + let prompts = stmt.query_map([app_type], |row| { + Ok(Prompt { + id: row.get(0)?, + app_type: row.get(1)?, + name: row.get(2)?, + content: row.get(3)?, + description: row.get(4)?, + enabled: row.get::<_, i32>(5)? == 1, + created_at: row.get(6)?, + updated_at: row.get(7)?, + }) + })?; + + prompts.collect() + } + + /// Get all prompts as a HashMap (id -> Prompt) + pub fn get_all_map( + conn: &Connection, + app_type: &str, + ) -> Result, rusqlite::Error> { + let prompts = Self::get_all(conn, app_type)?; + Ok(prompts.into_iter().map(|p| (p.id.clone(), p)).collect()) + } + + /// Get a single prompt by id + pub fn get_by_id( + conn: &Connection, + app_type: &str, + id: &str, + ) -> Result, rusqlite::Error> { + let mut stmt = conn.prepare( + "SELECT id, app_type, name, content, description, enabled, created_at, updated_at + FROM prompts WHERE app_type = ? AND id = ?", + )?; + + let mut rows = stmt.query([app_type, id])?; + + if let Some(row) = rows.next()? { + Ok(Some(Prompt { + id: row.get(0)?, + app_type: row.get(1)?, + name: row.get(2)?, + content: row.get(3)?, + description: row.get(4)?, + enabled: row.get::<_, i32>(5)? == 1, + created_at: row.get(6)?, + updated_at: row.get(7)?, + })) + } else { + Ok(None) + } + } + + /// Get the currently enabled prompt + pub fn get_enabled( + conn: &Connection, + app_type: &str, + ) -> Result, rusqlite::Error> { + let mut stmt = conn.prepare( + "SELECT id, app_type, name, content, description, enabled, created_at, updated_at + FROM prompts WHERE app_type = ? AND enabled = 1", + )?; + + let mut rows = stmt.query([app_type])?; + + if let Some(row) = rows.next()? { + Ok(Some(Prompt { + id: row.get(0)?, + app_type: row.get(1)?, + name: row.get(2)?, + content: row.get(3)?, + description: row.get(4)?, + enabled: row.get::<_, i32>(5)? == 1, + created_at: row.get(6)?, + updated_at: row.get(7)?, + })) + } else { + Ok(None) + } + } + + /// Insert or update a prompt (upsert) + pub fn upsert(conn: &Connection, prompt: &Prompt) -> Result<(), rusqlite::Error> { + conn.execute( + "INSERT INTO prompts (id, app_type, name, content, description, enabled, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) + ON CONFLICT(id, app_type) DO UPDATE SET + name = excluded.name, + content = excluded.content, + description = excluded.description, + enabled = excluded.enabled, + updated_at = excluded.updated_at", + params![ + prompt.id, + prompt.app_type, + prompt.name, + prompt.content, + prompt.description, + if prompt.enabled { 1 } else { 0 }, + prompt.created_at, + prompt.updated_at, + ], + )?; + Ok(()) + } + + /// Insert a new prompt + pub fn insert(conn: &Connection, prompt: &Prompt) -> Result<(), rusqlite::Error> { + conn.execute( + "INSERT INTO prompts (id, app_type, name, content, description, enabled, created_at, updated_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)", + params![ + prompt.id, + prompt.app_type, + prompt.name, + prompt.content, + prompt.description, + if prompt.enabled { 1 } else { 0 }, + prompt.created_at, + prompt.updated_at, + ], + )?; + Ok(()) + } + + /// Update an existing prompt + pub fn update(conn: &Connection, prompt: &Prompt) -> Result<(), rusqlite::Error> { + conn.execute( + "UPDATE prompts SET name = ?1, content = ?2, description = ?3, enabled = ?4, updated_at = ?5 + WHERE id = ?6 AND app_type = ?7", + params![ + prompt.name, + prompt.content, + prompt.description, + if prompt.enabled { 1 } else { 0 }, + prompt.updated_at, + prompt.id, + prompt.app_type, + ], + )?; + Ok(()) + } + + /// Delete a prompt + pub fn delete(conn: &Connection, app_type: &str, id: &str) -> Result<(), rusqlite::Error> { + conn.execute( + "DELETE FROM prompts WHERE app_type = ? AND id = ?", + [app_type, id], + )?; + Ok(()) + } + + /// Disable all prompts for an app type + pub fn disable_all(conn: &Connection, app_type: &str) -> Result<(), rusqlite::Error> { + conn.execute("UPDATE prompts SET enabled = 0 WHERE app_type = ?", [app_type])?; + Ok(()) + } + + /// Enable a specific prompt (and disable all others) + pub fn enable(conn: &Connection, app_type: &str, id: &str) -> Result<(), rusqlite::Error> { + // First disable all + Self::disable_all(conn, app_type)?; + // Then enable the specific one + conn.execute( + "UPDATE prompts SET enabled = 1 WHERE app_type = ? AND id = ?", + [app_type, id], + )?; + Ok(()) + } + + // Legacy method for compatibility + pub fn set_current(conn: &Connection, app_type: &str, id: &str) -> Result<(), rusqlite::Error> { + Self::enable(conn, app_type, id) + } +} diff --git a/src-tauri/src/database/dao/providers.rs b/src-tauri/src/database/dao/providers.rs index d82dcfbe9..2dc09d2c2 100644 --- a/src-tauri/src/database/dao/providers.rs +++ b/src-tauri/src/database/dao/providers.rs @@ -1,2 +1,173 @@ -// Providers DAO - 数据访问对象 -// TODO: 实现 providers 表的 CRUD 操作 +use crate::models::Provider; +use rusqlite::{params, Connection}; +use serde_json::Value; + +pub struct ProviderDao; + +impl ProviderDao { + pub fn get_all(conn: &Connection, app_type: &str) -> Result, rusqlite::Error> { + let mut stmt = conn.prepare( + "SELECT id, app_type, name, settings_config, category, icon, icon_color, + notes, created_at, sort_index, is_current + FROM providers WHERE app_type = ? ORDER BY sort_index, created_at", + )?; + + let providers = stmt.query_map([app_type], |row| { + let settings_str: String = row.get(3)?; + let settings_config: Value = serde_json::from_str(&settings_str).unwrap_or(Value::Null); + + Ok(Provider { + id: row.get(0)?, + app_type: row.get(1)?, + name: row.get(2)?, + settings_config, + category: row.get(4)?, + icon: row.get(5)?, + icon_color: row.get(6)?, + notes: row.get(7)?, + created_at: row.get(8)?, + sort_index: row.get(9)?, + is_current: row.get::<_, i32>(10)? == 1, + }) + })?; + + providers.collect() + } + + pub fn get_by_id( + conn: &Connection, + app_type: &str, + id: &str, + ) -> Result, rusqlite::Error> { + let mut stmt = conn.prepare( + "SELECT id, app_type, name, settings_config, category, icon, icon_color, + notes, created_at, sort_index, is_current + FROM providers WHERE app_type = ? AND id = ?", + )?; + + let result = stmt.query_row([app_type, id], |row| { + let settings_str: String = row.get(3)?; + let settings_config: Value = serde_json::from_str(&settings_str).unwrap_or(Value::Null); + + Ok(Provider { + id: row.get(0)?, + app_type: row.get(1)?, + name: row.get(2)?, + settings_config, + category: row.get(4)?, + icon: row.get(5)?, + icon_color: row.get(6)?, + notes: row.get(7)?, + created_at: row.get(8)?, + sort_index: row.get(9)?, + is_current: row.get::<_, i32>(10)? == 1, + }) + }); + + match result { + Ok(provider) => Ok(Some(provider)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e), + } + } + + pub fn insert(conn: &Connection, provider: &Provider) -> Result<(), rusqlite::Error> { + conn.execute( + "INSERT INTO providers (id, app_type, name, settings_config, category, icon, + icon_color, notes, created_at, sort_index, is_current) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)", + params![ + provider.id, + provider.app_type, + provider.name, + serde_json::to_string(&provider.settings_config).unwrap_or_default(), + provider.category, + provider.icon, + provider.icon_color, + provider.notes, + provider.created_at, + provider.sort_index, + if provider.is_current { 1 } else { 0 }, + ], + )?; + Ok(()) + } + + pub fn update(conn: &Connection, provider: &Provider) -> Result<(), rusqlite::Error> { + conn.execute( + "UPDATE providers SET name = ?1, settings_config = ?2, category = ?3, + icon = ?4, icon_color = ?5, notes = ?6, sort_index = ?7 + WHERE id = ?8 AND app_type = ?9", + params![ + provider.name, + serde_json::to_string(&provider.settings_config).unwrap_or_default(), + provider.category, + provider.icon, + provider.icon_color, + provider.notes, + provider.sort_index, + provider.id, + provider.app_type, + ], + )?; + Ok(()) + } + + pub fn delete(conn: &Connection, app_type: &str, id: &str) -> Result<(), rusqlite::Error> { + conn.execute( + "DELETE FROM providers WHERE app_type = ? AND id = ?", + [app_type, id], + )?; + Ok(()) + } + + pub fn set_current(conn: &Connection, app_type: &str, id: &str) -> Result<(), rusqlite::Error> { + // 先清除所有 is_current + conn.execute( + "UPDATE providers SET is_current = 0 WHERE app_type = ?", + [app_type], + )?; + // 设置新的 current + conn.execute( + "UPDATE providers SET is_current = 1 WHERE app_type = ? AND id = ?", + [app_type, id], + )?; + Ok(()) + } + + pub fn get_current( + conn: &Connection, + app_type: &str, + ) -> Result, rusqlite::Error> { + let mut stmt = conn.prepare( + "SELECT id, app_type, name, settings_config, category, icon, icon_color, + notes, created_at, sort_index, is_current + FROM providers WHERE app_type = ? AND is_current = 1", + )?; + + let result = stmt.query_row([app_type], |row| { + let settings_str: String = row.get(3)?; + let settings_config: Value = serde_json::from_str(&settings_str).unwrap_or(Value::Null); + + Ok(Provider { + id: row.get(0)?, + app_type: row.get(1)?, + name: row.get(2)?, + settings_config, + category: row.get(4)?, + icon: row.get(5)?, + icon_color: row.get(6)?, + notes: row.get(7)?, + created_at: row.get(8)?, + sort_index: row.get(9)?, + is_current: true, + }) + }); + + match result { + Ok(provider) => Ok(Some(provider)), + Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None), + Err(e) => Err(e), + } + } +} diff --git a/src-tauri/src/database/schema.rs b/src-tauri/src/database/schema.rs index 9016fc719..701ab9dfd 100644 --- a/src-tauri/src/database/schema.rs +++ b/src-tauri/src/database/schema.rs @@ -44,13 +44,20 @@ pub fn create_tables(conn: &Connection) -> Result<(), rusqlite::Error> { name TEXT NOT NULL, content TEXT NOT NULL, description TEXT, - is_current INTEGER DEFAULT 0, + enabled INTEGER DEFAULT 0, created_at INTEGER, + updated_at INTEGER, PRIMARY KEY (id, app_type) )", [], )?; + // Migration: rename is_current to enabled if old column exists + let _ = conn.execute("ALTER TABLE prompts RENAME COLUMN is_current TO enabled", []); + + // Migration: add updated_at column if it doesn't exist + let _ = conn.execute("ALTER TABLE prompts ADD COLUMN updated_at INTEGER", []); + // 设置表 conn.execute( "CREATE TABLE IF NOT EXISTS settings ( diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 86f8920ab..edb3da8e3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,4 @@ +mod commands; mod config; mod converter; mod database; @@ -5,6 +6,7 @@ mod logger; mod models; mod providers; mod server; +mod services; use serde::{Deserialize, Serialize}; use std::sync::Arc; @@ -1257,14 +1259,22 @@ pub fn run() { let state: AppState = Arc::new(RwLock::new(server::ServerState::new(config))); let logs: LogState = Arc::new(RwLock::new(logger::LogStore::new())); + // Initialize database for Switch functionality + let db = database::init_database().expect("Failed to initialize database"); + // Clone for setup hook let state_clone = state.clone(); let logs_clone = logs.clone(); tauri::Builder::default() .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_autostart::init( + tauri_plugin_autostart::MacosLauncher::LaunchAgent, + Some(vec!["--minimized"]), + )) .manage(state) .manage(logs) + .manage(db) .setup(move |_app| { // 自动启动服务器 let state = state_clone.clone(); @@ -1313,21 +1323,29 @@ pub fn run() { save_config, get_default_provider, set_default_provider, - // Kiro commands + // Unified OAuth commands (new) + commands::oauth_cmd::get_oauth_credentials, + commands::oauth_cmd::reload_oauth_credentials, + commands::oauth_cmd::refresh_oauth_token, + commands::oauth_cmd::get_oauth_env_variables, + commands::oauth_cmd::get_oauth_token_file_hash, + commands::oauth_cmd::check_and_reload_oauth_credentials, + commands::oauth_cmd::get_all_oauth_credentials, + // Legacy Kiro commands (deprecated, kept for compatibility) refresh_kiro_token, reload_credentials, get_kiro_credentials, get_env_variables, get_token_file_hash, check_and_reload_credentials, - // Gemini commands + // Legacy Gemini commands (deprecated, kept for compatibility) get_gemini_credentials, reload_gemini_credentials, refresh_gemini_token, get_gemini_env_variables, get_gemini_token_file_hash, check_and_reload_gemini_credentials, - // Qwen commands + // Legacy Qwen commands (deprecated, kept for compatibility) get_qwen_credentials, reload_qwen_credentials, refresh_qwen_token, @@ -1347,6 +1365,41 @@ pub fn run() { get_available_models, // API Compatibility check_api_compatibility, + // Switch commands + commands::switch_cmd::get_switch_providers, + commands::switch_cmd::get_current_switch_provider, + commands::switch_cmd::add_switch_provider, + commands::switch_cmd::update_switch_provider, + commands::switch_cmd::delete_switch_provider, + commands::switch_cmd::switch_provider, + commands::switch_cmd::import_default_config, + commands::switch_cmd::read_live_provider_settings, + // Config commands + commands::config_cmd::get_config_status, + commands::config_cmd::get_config_dir_path, + commands::config_cmd::open_config_folder, + commands::config_cmd::get_tool_versions, + commands::config_cmd::get_auto_launch_status, + commands::config_cmd::set_auto_launch, + // MCP commands + commands::mcp_cmd::get_mcp_servers, + commands::mcp_cmd::add_mcp_server, + commands::mcp_cmd::update_mcp_server, + commands::mcp_cmd::delete_mcp_server, + commands::mcp_cmd::toggle_mcp_server, + commands::mcp_cmd::import_mcp_from_app, + commands::mcp_cmd::sync_all_mcp_to_live, + // Prompt commands + commands::prompt_cmd::get_prompts, + commands::prompt_cmd::upsert_prompt, + commands::prompt_cmd::add_prompt, + commands::prompt_cmd::update_prompt, + commands::prompt_cmd::delete_prompt, + commands::prompt_cmd::enable_prompt, + commands::prompt_cmd::import_prompt_from_file, + commands::prompt_cmd::get_current_prompt_file_content, + commands::prompt_cmd::auto_import_prompt, + commands::prompt_cmd::switch_prompt, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/models/prompt_model.rs b/src-tauri/src/models/prompt_model.rs index 72f9e6682..a80d4682f 100644 --- a/src-tauri/src/models/prompt_model.rs +++ b/src-tauri/src/models/prompt_model.rs @@ -8,22 +8,27 @@ pub struct Prompt { pub content: String, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, + /// Whether this prompt is currently enabled (synced to live file) #[serde(default)] - pub is_current: bool, - #[serde(skip_serializing_if = "Option::is_none")] + pub enabled: bool, + #[serde(rename = "createdAt", skip_serializing_if = "Option::is_none")] pub created_at: Option, + #[serde(rename = "updatedAt", skip_serializing_if = "Option::is_none")] + pub updated_at: Option, } impl Prompt { pub fn new(id: String, app_type: String, name: String, content: String) -> Self { + let now = chrono::Utc::now().timestamp(); Self { id, app_type, name, content, description: None, - is_current: false, - created_at: Some(chrono::Utc::now().timestamp()), + enabled: false, + created_at: Some(now), + updated_at: Some(now), } } } diff --git a/src-tauri/src/server.rs b/src-tauri/src/server.rs index d80e47adf..d97afbf9e 100644 --- a/src-tauri/src/server.rs +++ b/src-tauri/src/server.rs @@ -356,19 +356,34 @@ async fn chat_completions( ) .into_response(), } - } else if status.as_u16() == 403 { - // Token 过期,尝试刷新 + } else if status.as_u16() == 403 || status.as_u16() == 402 { + // Token 过期或账户问题,尝试重新加载凭证并刷新 drop(kiro); let _guard = state.kiro_refresh_lock.lock().await; let mut kiro = state.kiro.write().await; - state - .logs - .write() - .await - .add("warn", "Got 403, attempting token refresh"); + state.logs.write().await.add( + "warn", + &format!( + "[AUTH] Got {}, reloading credentials and attempting token refresh...", + status.as_u16() + ), + ); + + // 先重新加载凭证文件(可能用户换了账户) + if let Err(e) = kiro.load_credentials().await { + state.logs.write().await.add( + "error", + &format!("[AUTH] Failed to reload credentials: {e}"), + ); + } match kiro.refresh_token().await { Ok(_) => { + state + .logs + .write() + .await + .add("info", "[AUTH] Token refreshed successfully after reload"); // 重试请求 drop(kiro); let kiro = state.kiro.read().await; @@ -438,13 +453,22 @@ async fn chat_completions( Err(e) => ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": {"message": e.to_string()}})), - ).into_response(), + ) + .into_response(), } } - Err(e) => ( - StatusCode::UNAUTHORIZED, - Json(serde_json::json!({"error": {"message": format!("Token refresh failed: {e}")}})), - ).into_response(), + Err(e) => { + state + .logs + .write() + .await + .add("error", &format!("[AUTH] Token refresh failed: {e}")); + ( + StatusCode::UNAUTHORIZED, + Json(serde_json::json!({"error": {"message": format!("Token refresh failed: {e}")}})), + ) + .into_response() + } } } else { let body = resp.text().await.unwrap_or_default(); @@ -655,23 +679,33 @@ async fn anthropic_messages( .into_response() } } - } else if status.as_u16() == 403 { - // Token 过期,尝试刷新 + } else if status.as_u16() == 403 || status.as_u16() == 402 { + // Token 过期或账户问题,尝试重新加载凭证并刷新 drop(kiro); let _guard = state.kiro_refresh_lock.lock().await; let mut kiro = state.kiro.write().await; state.logs.write().await.add( "warn", - "[AUTH] Got 403 Forbidden, attempting token refresh...", + &format!( + "[AUTH] Got {}, reloading credentials and attempting token refresh...", + status.as_u16() + ), ); + // 先重新加载凭证文件(可能用户换了账户) + if let Err(e) = kiro.load_credentials().await { + state.logs.write().await.add( + "error", + &format!("[AUTH] Failed to reload credentials: {e}"), + ); + } + match kiro.refresh_token().await { Ok(_) => { - state - .logs - .write() - .await - .add("info", "[AUTH] Token refreshed, retrying request..."); + state.logs.write().await.add( + "info", + "[AUTH] Token refreshed successfully, retrying request...", + ); drop(kiro); let kiro = state.kiro.read().await; match kiro.call_api(&openai_request).await { diff --git a/src-tauri/src/services/live_sync.rs b/src-tauri/src/services/live_sync.rs new file mode 100644 index 000000000..0da32831d --- /dev/null +++ b/src-tauri/src/services/live_sync.rs @@ -0,0 +1,230 @@ +use crate::models::{AppType, Provider}; +use serde_json::{json, Value}; +use std::path::PathBuf; + +/// Get the configuration file path for an app type +pub fn get_app_config_path(app_type: &AppType) -> Option { + let home = dirs::home_dir()?; + match app_type { + AppType::Claude => Some(home.join(".claude").join("settings.json")), + AppType::Codex => Some(home.join(".codex")), + AppType::Gemini => Some(home.join(".gemini")), + AppType::ProxyCast => None, + } +} + +/// Sync provider configuration to live config files +pub fn sync_to_live( + app_type: &AppType, + provider: &Provider, +) -> Result<(), Box> { + match app_type { + AppType::Claude => sync_claude_settings(provider), + AppType::Codex => sync_codex_config(provider), + AppType::Gemini => sync_gemini_config(provider), + AppType::ProxyCast => Ok(()), + } +} + +/// Sync Claude settings to ~/.claude/settings.json +fn sync_claude_settings( + provider: &Provider, +) -> Result<(), Box> { + let home = dirs::home_dir().ok_or("Cannot find home directory")?; + let claude_dir = home.join(".claude"); + let config_path = claude_dir.join("settings.json"); + + // Create directory if not exists + std::fs::create_dir_all(&claude_dir)?; + + // Read existing settings to preserve other fields + let mut settings: Value = if config_path.exists() { + let content = std::fs::read_to_string(&config_path)?; + serde_json::from_str(&content).unwrap_or_else(|_| json!({})) + } else { + json!({}) + }; + + // Merge env variables into settings + if let Some(env_obj) = provider.settings_config.get("env").and_then(|v| v.as_object()) { + let settings_obj = settings.as_object_mut().ok_or("Invalid settings format")?; + + // Ensure env object exists + if !settings_obj.contains_key("env") { + settings_obj.insert("env".to_string(), json!({})); + } + + if let Some(target_env) = settings_obj.get_mut("env").and_then(|v| v.as_object_mut()) { + for (key, value) in env_obj { + target_env.insert(key.clone(), value.clone()); + } + } + } else { + // If settings_config is the full settings object, use it directly + settings = provider.settings_config.clone(); + } + + // Write settings + let content = serde_json::to_string_pretty(&settings)?; + std::fs::write(&config_path, content)?; + + Ok(()) +} + +/// Sync Codex config to ~/.codex/auth.json and ~/.codex/config.toml +fn sync_codex_config( + provider: &Provider, +) -> Result<(), Box> { + let home = dirs::home_dir().ok_or("Cannot find home directory")?; + let codex_dir = home.join(".codex"); + + // Create directory if not exists + std::fs::create_dir_all(&codex_dir)?; + + if let Some(obj) = provider.settings_config.as_object() { + // Write auth.json + if let Some(auth) = obj.get("auth") { + let auth_path = codex_dir.join("auth.json"); + let content = serde_json::to_string_pretty(auth)?; + std::fs::write(&auth_path, content)?; + } + + // Write config.toml + if let Some(config) = obj.get("config").and_then(|v| v.as_str()) { + let config_path = codex_dir.join("config.toml"); + std::fs::write(&config_path, config)?; + } + } + + Ok(()) +} + +/// Sync Gemini config to ~/.gemini/.env and ~/.gemini/settings.json +fn sync_gemini_config( + provider: &Provider, +) -> Result<(), Box> { + let home = dirs::home_dir().ok_or("Cannot find home directory")?; + let gemini_dir = home.join(".gemini"); + + // Create directory if not exists + std::fs::create_dir_all(&gemini_dir)?; + + // Write .env file + if let Some(env_obj) = provider.settings_config.get("env").and_then(|v| v.as_object()) { + let env_path = gemini_dir.join(".env"); + let mut content = String::new(); + + for (key, value) in env_obj { + if let Some(val) = value.as_str() { + // Only write non-empty values + if !val.is_empty() { + content.push_str(&format!("{}={}\n", key, val)); + } + } + } + + std::fs::write(&env_path, content)?; + } + + // Write settings.json (for MCP servers and other config) + if let Some(config) = provider.settings_config.get("config") { + if config.is_object() { + let settings_path = gemini_dir.join("settings.json"); + + // Read existing settings to preserve mcpServers + let mut settings: Value = if settings_path.exists() { + let content = std::fs::read_to_string(&settings_path)?; + serde_json::from_str(&content).unwrap_or_else(|_| json!({})) + } else { + json!({}) + }; + + // Merge config into settings + if let (Some(settings_obj), Some(config_obj)) = + (settings.as_object_mut(), config.as_object()) + { + for (key, value) in config_obj { + settings_obj.insert(key.clone(), value.clone()); + } + } + + let content = serde_json::to_string_pretty(&settings)?; + std::fs::write(&settings_path, content)?; + } + } + + Ok(()) +} + +/// Read current live settings for an app type +pub fn read_live_settings(app_type: &AppType) -> Result> { + let home = dirs::home_dir().ok_or("Cannot find home directory")?; + + match app_type { + AppType::Claude => { + let path = home.join(".claude").join("settings.json"); + if !path.exists() { + return Err("Claude settings file not found".into()); + } + let content = std::fs::read_to_string(&path)?; + Ok(serde_json::from_str(&content)?) + } + AppType::Codex => { + let codex_dir = home.join(".codex"); + let auth_path = codex_dir.join("auth.json"); + let config_path = codex_dir.join("config.toml"); + + let auth: Value = if auth_path.exists() { + let content = std::fs::read_to_string(&auth_path)?; + serde_json::from_str(&content)? + } else { + json!({}) + }; + + let config = if config_path.exists() { + std::fs::read_to_string(&config_path)? + } else { + String::new() + }; + + Ok(json!({ + "auth": auth, + "config": config + })) + } + AppType::Gemini => { + let gemini_dir = home.join(".gemini"); + let env_path = gemini_dir.join(".env"); + let settings_path = gemini_dir.join("settings.json"); + + // Read .env file + let mut env_map: serde_json::Map = serde_json::Map::new(); + if env_path.exists() { + let content = std::fs::read_to_string(&env_path)?; + for line in content.lines() { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') { + continue; + } + if let Some((key, value)) = line.split_once('=') { + env_map.insert(key.trim().to_string(), json!(value.trim())); + } + } + } + + // Read settings.json + let config: Value = if settings_path.exists() { + let content = std::fs::read_to_string(&settings_path)?; + serde_json::from_str(&content)? + } else { + json!({}) + }; + + Ok(json!({ + "env": env_map, + "config": config + })) + } + AppType::ProxyCast => Ok(json!({})), + } +} diff --git a/src-tauri/src/services/mcp_service.rs b/src-tauri/src/services/mcp_service.rs new file mode 100644 index 000000000..004293b97 --- /dev/null +++ b/src-tauri/src/services/mcp_service.rs @@ -0,0 +1,110 @@ +use crate::database::dao::mcp::McpDao; +use crate::database::DbConnection; +use crate::models::{AppType, McpServer}; +use crate::services::mcp_sync; + +pub struct McpService; + +impl McpService { + pub fn get_all(db: &DbConnection) -> Result, String> { + let conn = db.lock().map_err(|e| e.to_string())?; + McpDao::get_all(&conn).map_err(|e| e.to_string()) + } + + pub fn add(db: &DbConnection, server: McpServer) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + McpDao::insert(&conn, &server).map_err(|e| e.to_string())?; + + // Sync to enabled apps + let servers = McpDao::get_all(&conn).map_err(|e| e.to_string())?; + mcp_sync::sync_all_mcp_to_live(&servers).map_err(|e| e.to_string())?; + + Ok(()) + } + + pub fn update(db: &DbConnection, server: McpServer) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + McpDao::update(&conn, &server).map_err(|e| e.to_string())?; + + // Sync to enabled apps + let servers = McpDao::get_all(&conn).map_err(|e| e.to_string())?; + mcp_sync::sync_all_mcp_to_live(&servers).map_err(|e| e.to_string())?; + + Ok(()) + } + + pub fn delete(db: &DbConnection, id: &str) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + McpDao::delete(&conn, id).map_err(|e| e.to_string())?; + + // Remove from all apps + mcp_sync::remove_mcp_from_all_apps(id).map_err(|e| e.to_string())?; + + Ok(()) + } + + pub fn toggle_enabled( + db: &DbConnection, + id: &str, + app_type: &str, + enabled: bool, + ) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + McpDao::toggle_enabled(&conn, id, app_type, enabled).map_err(|e| e.to_string())?; + + // Get the server and sync + let servers = McpDao::get_all(&conn).map_err(|e| e.to_string())?; + let server = servers.iter().find(|s| s.id == id); + + if let Some(server) = server { + let app = app_type.parse::().map_err(|e| e.to_string())?; + if enabled { + // Sync server to the app + mcp_sync::sync_mcp_to_app(&app, &servers).map_err(|e| e.to_string())?; + } else { + // Remove server from the app + mcp_sync::remove_mcp_from_app(&app, id).map_err(|e| e.to_string())?; + } + } + + Ok(()) + } + + /// Sync all enabled MCP servers to all apps + pub fn sync_all_to_live(db: &DbConnection) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + let servers = McpDao::get_all(&conn).map_err(|e| e.to_string())?; + mcp_sync::sync_all_mcp_to_live(&servers).map_err(|e| e.to_string()) + } + + /// Import MCP servers from an app + pub fn import_from_app(db: &DbConnection, app_type: &str) -> Result { + let app = app_type.parse::().map_err(|e| e.to_string())?; + let conn = db.lock().map_err(|e| e.to_string())?; + + // Get existing servers + let existing = McpDao::get_all(&conn).map_err(|e| e.to_string())?; + let existing_ids: std::collections::HashSet = + existing.iter().map(|s| s.id.clone()).collect(); + + // Import from app + let imported = mcp_sync::import_mcp_from_app(&app).map_err(|e| e.to_string())?; + + let mut count = 0; + for server in imported { + if existing_ids.contains(&server.id) { + // Update existing server's enabled status for this app + if McpDao::toggle_enabled(&conn, &server.id, app_type, true).is_ok() { + count += 1; + } + } else { + // Insert new server + if McpDao::insert(&conn, &server).is_ok() { + count += 1; + } + } + } + + Ok(count) + } +} diff --git a/src-tauri/src/services/mcp_sync.rs b/src-tauri/src/services/mcp_sync.rs new file mode 100644 index 000000000..7727a66b0 --- /dev/null +++ b/src-tauri/src/services/mcp_sync.rs @@ -0,0 +1,536 @@ +use crate::models::{AppType, McpServer}; +use serde_json::{json, Map, Value}; +use std::path::PathBuf; + +/// Get the MCP config file path for an app type +pub fn get_mcp_config_path(app_type: &AppType) -> Option { + let home = dirs::home_dir()?; + match app_type { + AppType::Claude => Some(home.join(".claude").join("settings.json")), + AppType::Codex => Some(home.join(".codex").join("config.toml")), + AppType::Gemini => Some(home.join(".gemini").join("settings.json")), + AppType::ProxyCast => None, + } +} + +/// Sync all enabled MCP servers to their respective app configurations +pub fn sync_all_mcp_to_live( + servers: &[McpServer], +) -> Result<(), Box> { + // Sync to Claude + let claude_servers: Vec<&McpServer> = servers.iter().filter(|s| s.enabled_claude).collect(); + sync_mcp_to_claude(&claude_servers)?; + + // Sync to Codex + let codex_servers: Vec<&McpServer> = servers.iter().filter(|s| s.enabled_codex).collect(); + sync_mcp_to_codex(&codex_servers)?; + + // Sync to Gemini + let gemini_servers: Vec<&McpServer> = servers.iter().filter(|s| s.enabled_gemini).collect(); + sync_mcp_to_gemini(&gemini_servers)?; + + Ok(()) +} + +/// Sync MCP servers to a specific app +pub fn sync_mcp_to_app( + app_type: &AppType, + servers: &[McpServer], +) -> Result<(), Box> { + let enabled_servers: Vec<&McpServer> = servers + .iter() + .filter(|s| match app_type { + AppType::Claude => s.enabled_claude, + AppType::Codex => s.enabled_codex, + AppType::Gemini => s.enabled_gemini, + AppType::ProxyCast => s.enabled_proxycast, + }) + .collect(); + + match app_type { + AppType::Claude => sync_mcp_to_claude(&enabled_servers), + AppType::Codex => sync_mcp_to_codex(&enabled_servers), + AppType::Gemini => sync_mcp_to_gemini(&enabled_servers), + AppType::ProxyCast => Ok(()), + } +} + +/// Sync MCP servers to Claude's settings.json +/// Claude uses the mcpServers field in ~/.claude/settings.json +fn sync_mcp_to_claude( + servers: &[&McpServer], +) -> Result<(), Box> { + let home = dirs::home_dir().ok_or("Cannot find home directory")?; + let claude_dir = home.join(".claude"); + let config_path = claude_dir.join("settings.json"); + + // Create directory if not exists + std::fs::create_dir_all(&claude_dir)?; + + // Read existing settings + let mut settings: Value = if config_path.exists() { + let content = std::fs::read_to_string(&config_path)?; + serde_json::from_str(&content).unwrap_or_else(|_| json!({})) + } else { + json!({}) + }; + + // Build mcpServers object - use name as key (not id which may be UUID) + let mut mcp_servers = Map::new(); + for server in servers { + if let Some(config) = server.server_config.as_object() { + mcp_servers.insert(server.name.clone(), Value::Object(config.clone())); + } + } + + // Update settings with mcpServers + if let Some(obj) = settings.as_object_mut() { + obj.insert("mcpServers".to_string(), Value::Object(mcp_servers)); + } + + // Write settings + let content = serde_json::to_string_pretty(&settings)?; + std::fs::write(&config_path, content)?; + + Ok(()) +} + +/// Sync MCP servers to Codex's config.toml +/// Codex uses [mcp_servers.*] sections in ~/.codex/config.toml +fn sync_mcp_to_codex( + servers: &[&McpServer], +) -> Result<(), Box> { + let home = dirs::home_dir().ok_or("Cannot find home directory")?; + let codex_dir = home.join(".codex"); + let config_path = codex_dir.join("config.toml"); + + // Create directory if not exists + std::fs::create_dir_all(&codex_dir)?; + + // Read existing config + let mut existing_content = if config_path.exists() { + std::fs::read_to_string(&config_path)? + } else { + String::new() + }; + + // Remove existing [mcp_servers.*] sections + let mut lines: Vec<&str> = existing_content.lines().collect(); + let mut new_lines: Vec = Vec::new(); + let mut in_mcp_section = false; + + for line in &lines { + let trimmed = line.trim(); + if trimmed.starts_with("[mcp_servers.") { + in_mcp_section = true; + continue; + } + if in_mcp_section && trimmed.starts_with('[') { + in_mcp_section = false; + } + if !in_mcp_section { + new_lines.push(line.to_string()); + } + } + + // Add new MCP server sections - use name as key + for server in servers { + new_lines.push(String::new()); + new_lines.push(format!("[mcp_servers.{}]", server.name)); + + if let Some(config) = server.server_config.as_object() { + // Convert JSON config to TOML format + if let Some(command) = config.get("command").and_then(|v| v.as_str()) { + new_lines.push(format!("command = \"{}\"", command)); + } + + if let Some(args) = config.get("args").and_then(|v| v.as_array()) { + let args_str: Vec = args + .iter() + .filter_map(|a| a.as_str()) + .map(|s| format!("\"{}\"", s)) + .collect(); + new_lines.push(format!("args = [{}]", args_str.join(", "))); + } + + if let Some(env) = config.get("env").and_then(|v| v.as_object()) { + new_lines.push("[mcp_servers.".to_string() + &server.name + ".env]"); + for (key, value) in env { + if let Some(val) = value.as_str() { + new_lines.push(format!("{} = \"{}\"", key, val)); + } + } + } + } + } + + // Write config + let content = new_lines.join("\n"); + std::fs::write(&config_path, content)?; + + Ok(()) +} + +/// Sync MCP servers to Gemini's settings.json +/// Gemini uses the mcpServers field in ~/.gemini/settings.json +fn sync_mcp_to_gemini( + servers: &[&McpServer], +) -> Result<(), Box> { + let home = dirs::home_dir().ok_or("Cannot find home directory")?; + let gemini_dir = home.join(".gemini"); + let settings_path = gemini_dir.join("settings.json"); + + // Create directory if not exists + std::fs::create_dir_all(&gemini_dir)?; + + // Read existing settings + let mut settings: Value = if settings_path.exists() { + let content = std::fs::read_to_string(&settings_path)?; + serde_json::from_str(&content).unwrap_or_else(|_| json!({})) + } else { + json!({}) + }; + + // Build mcpServers object - use name as key + let mut mcp_servers = Map::new(); + for server in servers { + if let Some(config) = server.server_config.as_object() { + mcp_servers.insert(server.name.clone(), Value::Object(config.clone())); + } + } + + // Update settings with mcpServers + if let Some(obj) = settings.as_object_mut() { + obj.insert("mcpServers".to_string(), Value::Object(mcp_servers)); + } + + // Write settings + let content = serde_json::to_string_pretty(&settings)?; + std::fs::write(&settings_path, content)?; + + Ok(()) +} + +/// Remove a specific MCP server from an app's config +pub fn remove_mcp_from_app( + app_type: &AppType, + server_id: &str, +) -> Result<(), Box> { + match app_type { + AppType::Claude => remove_mcp_from_claude(server_id), + AppType::Codex => remove_mcp_from_codex(server_id), + AppType::Gemini => remove_mcp_from_gemini(server_id), + AppType::ProxyCast => Ok(()), + } +} + +fn remove_mcp_from_claude( + server_id: &str, +) -> Result<(), Box> { + let home = dirs::home_dir().ok_or("Cannot find home directory")?; + let config_path = home.join(".claude").join("settings.json"); + + if !config_path.exists() { + return Ok(()); + } + + let content = std::fs::read_to_string(&config_path)?; + let mut settings: Value = serde_json::from_str(&content)?; + + if let Some(mcp_servers) = settings + .as_object_mut() + .and_then(|o| o.get_mut("mcpServers")) + .and_then(|v| v.as_object_mut()) + { + mcp_servers.remove(server_id); + } + + let content = serde_json::to_string_pretty(&settings)?; + std::fs::write(&config_path, content)?; + + Ok(()) +} + +fn remove_mcp_from_codex( + server_id: &str, +) -> Result<(), Box> { + let home = dirs::home_dir().ok_or("Cannot find home directory")?; + let config_path = home.join(".codex").join("config.toml"); + + if !config_path.exists() { + return Ok(()); + } + + let content = std::fs::read_to_string(&config_path)?; + let mut lines: Vec<&str> = content.lines().collect(); + let mut new_lines: Vec = Vec::new(); + let section_header = format!("[mcp_servers.{}]", server_id); + let env_header = format!("[mcp_servers.{}.env]", server_id); + let mut skip_section = false; + + for line in &lines { + let trimmed = line.trim(); + + // Check if this is the section we want to remove + if trimmed == section_header || trimmed == env_header { + skip_section = true; + continue; + } + + // Check if we've reached a new section + if skip_section && trimmed.starts_with('[') { + skip_section = false; + } + + if !skip_section { + new_lines.push(line.to_string()); + } + } + + let content = new_lines.join("\n"); + std::fs::write(&config_path, content)?; + + Ok(()) +} + +fn remove_mcp_from_gemini( + server_id: &str, +) -> Result<(), Box> { + let home = dirs::home_dir().ok_or("Cannot find home directory")?; + let settings_path = home.join(".gemini").join("settings.json"); + + if !settings_path.exists() { + return Ok(()); + } + + let content = std::fs::read_to_string(&settings_path)?; + let mut settings: Value = serde_json::from_str(&content)?; + + if let Some(mcp_servers) = settings + .as_object_mut() + .and_then(|o| o.get_mut("mcpServers")) + .and_then(|v| v.as_object_mut()) + { + mcp_servers.remove(server_id); + } + + let content = serde_json::to_string_pretty(&settings)?; + std::fs::write(&settings_path, content)?; + + Ok(()) +} + +/// Remove a specific MCP server from all apps +pub fn remove_mcp_from_all_apps( + server_id: &str, +) -> Result<(), Box> { + remove_mcp_from_claude(server_id)?; + remove_mcp_from_codex(server_id)?; + remove_mcp_from_gemini(server_id)?; + Ok(()) +} + +/// Import MCP servers from Claude's settings.json +pub fn import_mcp_from_claude() -> Result, Box> { + let home = dirs::home_dir().ok_or("Cannot find home directory")?; + let config_path = home.join(".claude").join("settings.json"); + + if !config_path.exists() { + return Ok(Vec::new()); + } + + let content = std::fs::read_to_string(&config_path)?; + let settings: Value = serde_json::from_str(&content)?; + + let mut servers = Vec::new(); + + if let Some(mcp_servers) = settings.get("mcpServers").and_then(|v| v.as_object()) { + for (id, config) in mcp_servers { + let server = crate::models::McpServer { + id: id.clone(), + name: id.clone(), + server_config: config.clone(), + description: None, + enabled_proxycast: false, + enabled_claude: true, + enabled_codex: false, + enabled_gemini: false, + created_at: Some(chrono::Utc::now().timestamp()), + }; + servers.push(server); + } + } + + Ok(servers) +} + +/// Import MCP servers from Codex's config.toml +pub fn import_mcp_from_codex() -> Result, Box> { + let home = dirs::home_dir().ok_or("Cannot find home directory")?; + let config_path = home.join(".codex").join("config.toml"); + + if !config_path.exists() { + return Ok(Vec::new()); + } + + let content = std::fs::read_to_string(&config_path)?; + let mut servers = Vec::new(); + let mut current_server_id: Option = None; + let mut current_config: Map = Map::new(); + let mut current_env: Map = Map::new(); + let mut in_env_section = false; + + for line in content.lines() { + let trimmed = line.trim(); + + // Check for [mcp_servers.name] section + if trimmed.starts_with("[mcp_servers.") && trimmed.ends_with(']') { + // Save previous server if any + if let Some(ref id) = current_server_id { + if !current_env.is_empty() { + current_config.insert("env".to_string(), Value::Object(current_env.clone())); + } + let server = crate::models::McpServer { + id: id.clone(), + name: id.clone(), + server_config: Value::Object(current_config.clone()), + description: None, + enabled_proxycast: false, + enabled_claude: false, + enabled_codex: true, + enabled_gemini: false, + created_at: Some(chrono::Utc::now().timestamp()), + }; + servers.push(server); + } + + // Parse new server ID + let section = &trimmed[13..trimmed.len() - 1]; // Remove "[mcp_servers." and "]" + if section.ends_with(".env") { + in_env_section = true; + } else { + current_server_id = Some(section.to_string()); + current_config = Map::new(); + current_env = Map::new(); + in_env_section = false; + } + continue; + } + + // Check for other sections + if trimmed.starts_with('[') { + // Save previous server if any + if let Some(ref id) = current_server_id { + if !current_env.is_empty() { + current_config.insert("env".to_string(), Value::Object(current_env.clone())); + } + let server = crate::models::McpServer { + id: id.clone(), + name: id.clone(), + server_config: Value::Object(current_config.clone()), + description: None, + enabled_proxycast: false, + enabled_claude: false, + enabled_codex: true, + enabled_gemini: false, + created_at: Some(chrono::Utc::now().timestamp()), + }; + servers.push(server); + current_server_id = None; + } + in_env_section = false; + continue; + } + + // Parse key = value + if current_server_id.is_some() && trimmed.contains('=') { + if let Some((key, value)) = trimmed.split_once('=') { + let key = key.trim(); + let value = value.trim().trim_matches('"'); + + if in_env_section { + current_env.insert(key.to_string(), Value::String(value.to_string())); + } else if key == "command" { + current_config.insert(key.to_string(), Value::String(value.to_string())); + } else if key == "args" { + // Parse array: ["arg1", "arg2"] + if value.starts_with('[') && value.ends_with(']') { + let args_str = &value[1..value.len() - 1]; + let args: Vec = args_str + .split(',') + .map(|s| Value::String(s.trim().trim_matches('"').to_string())) + .collect(); + current_config.insert(key.to_string(), Value::Array(args)); + } + } + } + } + } + + // Save last server if any + if let Some(ref id) = current_server_id { + if !current_env.is_empty() { + current_config.insert("env".to_string(), Value::Object(current_env)); + } + let server = crate::models::McpServer { + id: id.clone(), + name: id.clone(), + server_config: Value::Object(current_config), + description: None, + enabled_proxycast: false, + enabled_claude: false, + enabled_codex: true, + enabled_gemini: false, + created_at: Some(chrono::Utc::now().timestamp()), + }; + servers.push(server); + } + + Ok(servers) +} + +/// Import MCP servers from Gemini's settings.json +pub fn import_mcp_from_gemini() -> Result, Box> { + let home = dirs::home_dir().ok_or("Cannot find home directory")?; + let settings_path = home.join(".gemini").join("settings.json"); + + if !settings_path.exists() { + return Ok(Vec::new()); + } + + let content = std::fs::read_to_string(&settings_path)?; + let settings: Value = serde_json::from_str(&content)?; + + let mut servers = Vec::new(); + + if let Some(mcp_servers) = settings.get("mcpServers").and_then(|v| v.as_object()) { + for (id, config) in mcp_servers { + let server = crate::models::McpServer { + id: id.clone(), + name: id.clone(), + server_config: config.clone(), + description: None, + enabled_proxycast: false, + enabled_claude: false, + enabled_codex: false, + enabled_gemini: true, + created_at: Some(chrono::Utc::now().timestamp()), + }; + servers.push(server); + } + } + + Ok(servers) +} + +/// Import MCP servers from a specific app +pub fn import_mcp_from_app( + app_type: &AppType, +) -> Result, Box> { + match app_type { + AppType::Claude => import_mcp_from_claude(), + AppType::Codex => import_mcp_from_codex(), + AppType::Gemini => import_mcp_from_gemini(), + AppType::ProxyCast => Ok(Vec::new()), + } +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs new file mode 100644 index 000000000..ee4bd99c0 --- /dev/null +++ b/src-tauri/src/services/mod.rs @@ -0,0 +1,6 @@ +pub mod switch; +pub mod live_sync; +pub mod mcp_service; +pub mod mcp_sync; +pub mod prompt_service; +pub mod prompt_sync; diff --git a/src-tauri/src/services/prompt_service.rs b/src-tauri/src/services/prompt_service.rs new file mode 100644 index 000000000..7b1bbbbda --- /dev/null +++ b/src-tauri/src/services/prompt_service.rs @@ -0,0 +1,232 @@ +use crate::database::dao::prompts::PromptDao; +use crate::database::DbConnection; +use crate::models::{AppType, Prompt}; +use crate::services::prompt_sync; +use std::collections::HashMap; + +pub struct PromptService; + +impl PromptService { + /// Get all prompts for an app type + pub fn get_all(db: &DbConnection, app_type: &str) -> Result, String> { + let conn = db.lock().map_err(|e| e.to_string())?; + PromptDao::get_all(&conn, app_type).map_err(|e| e.to_string()) + } + + /// Get all prompts as a HashMap (for frontend) + pub fn get_all_map(db: &DbConnection, app_type: &str) -> Result, String> { + let conn = db.lock().map_err(|e| e.to_string())?; + PromptDao::get_all_map(&conn, app_type).map_err(|e| e.to_string()) + } + + /// Upsert a prompt (insert or update) + /// If the prompt is enabled, sync to live file + pub fn upsert(db: &DbConnection, app_type: &str, prompt: Prompt) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + PromptDao::upsert(&conn, &prompt).map_err(|e| e.to_string())?; + + // If this prompt is enabled, sync to live file + if prompt.enabled { + let app = app_type.parse::().map_err(|e| e.to_string())?; + prompt_sync::write_live_prompt(&app, &prompt.content)?; + } + + Ok(()) + } + + /// Add a new prompt + pub fn add(db: &DbConnection, prompt: Prompt) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + PromptDao::insert(&conn, &prompt).map_err(|e| e.to_string()) + } + + /// Update an existing prompt + /// If the prompt is enabled, sync to live file + pub fn update(db: &DbConnection, prompt: Prompt) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + PromptDao::update(&conn, &prompt).map_err(|e| e.to_string())?; + + // If this prompt is enabled, sync to live file + if prompt.enabled { + let app = prompt.app_type.parse::().map_err(|e| e.to_string())?; + prompt_sync::write_live_prompt(&app, &prompt.content)?; + } + + Ok(()) + } + + /// Delete a prompt + /// Cannot delete an enabled prompt + pub fn delete(db: &DbConnection, app_type: &str, id: &str) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + + // Check if prompt is enabled + if let Ok(Some(prompt)) = PromptDao::get_by_id(&conn, app_type, id) { + if prompt.enabled { + return Err("Cannot delete an enabled prompt. Disable it first.".to_string()); + } + } + + PromptDao::delete(&conn, app_type, id).map_err(|e| e.to_string()) + } + + /// Enable a prompt and sync to live file + /// This will: + /// 1. Backfill the current live file content to the currently enabled prompt (if any) + /// 2. Disable all other prompts + /// 3. Enable the specified prompt + /// 4. Write the prompt content to the live file + pub fn enable(db: &DbConnection, app_type: &str, id: &str) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + let app = app_type.parse::().map_err(|e| e.to_string())?; + + // Step 1: Backfill current live content to the currently enabled prompt + if let Ok(Some(live_content)) = prompt_sync::read_live_prompt(&app) { + if !live_content.trim().is_empty() { + if let Ok(Some(mut current_enabled)) = PromptDao::get_enabled(&conn, app_type) { + // Update the current enabled prompt with live content + current_enabled.content = live_content.clone(); + current_enabled.updated_at = Some(chrono::Utc::now().timestamp()); + let _ = PromptDao::update(&conn, ¤t_enabled); + tracing::info!( + "Backfilled live content to enabled prompt: {}", + current_enabled.id + ); + } else { + // No enabled prompt, check if we should create a backup + let prompts = PromptDao::get_all(&conn, app_type).map_err(|e| e.to_string())?; + let content_exists = prompts.iter().any(|p| p.content.trim() == live_content.trim()); + + if !content_exists { + // Create a backup prompt + let timestamp = chrono::Utc::now().timestamp(); + let backup = Prompt { + id: format!("backup-{}", timestamp), + app_type: app_type.to_string(), + name: format!( + "Original Prompt {}", + chrono::Local::now().format("%Y-%m-%d %H:%M") + ), + content: live_content, + description: Some("Auto-backup of original prompt".to_string()), + enabled: false, + created_at: Some(timestamp), + updated_at: Some(timestamp), + }; + let _ = PromptDao::insert(&conn, &backup); + tracing::info!("Created backup prompt: {}", backup.id); + } + } + } + } + + // Step 2 & 3: Enable the specified prompt (this also disables others) + PromptDao::enable(&conn, app_type, id).map_err(|e| e.to_string())?; + + // Step 4: Write to live file + if let Ok(Some(prompt)) = PromptDao::get_by_id(&conn, app_type, id) { + prompt_sync::write_live_prompt(&app, &prompt.content)?; + tracing::info!("Synced prompt {} to live file", id); + } + + Ok(()) + } + + /// Disable a prompt + pub fn disable(db: &DbConnection, app_type: &str, id: &str) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + + // Get the prompt and set enabled to false + if let Ok(Some(mut prompt)) = PromptDao::get_by_id(&conn, app_type, id) { + prompt.enabled = false; + prompt.updated_at = Some(chrono::Utc::now().timestamp()); + PromptDao::update(&conn, &prompt).map_err(|e| e.to_string())?; + } + + Ok(()) + } + + /// Import prompt from live file + pub fn import_from_file(db: &DbConnection, app_type: &str) -> Result { + let app = app_type.parse::().map_err(|e| e.to_string())?; + let content = prompt_sync::read_live_prompt(&app)? + .ok_or_else(|| "Prompt file does not exist or is empty".to_string())?; + + if content.trim().is_empty() { + return Err("Prompt file is empty".to_string()); + } + + let timestamp = chrono::Utc::now().timestamp(); + let id = format!("imported-{}", timestamp); + + let prompt = Prompt { + id: id.clone(), + app_type: app_type.to_string(), + name: format!( + "Imported Prompt {}", + chrono::Local::now().format("%Y-%m-%d %H:%M") + ), + content, + description: Some("Imported from existing config file".to_string()), + enabled: false, + created_at: Some(timestamp), + updated_at: Some(timestamp), + }; + + let conn = db.lock().map_err(|e| e.to_string())?; + PromptDao::insert(&conn, &prompt).map_err(|e| e.to_string())?; + + Ok(id) + } + + /// Get current live file content + pub fn get_live_content(app_type: &str) -> Result, String> { + let app = app_type.parse::().map_err(|e| e.to_string())?; + prompt_sync::read_live_prompt(&app) + } + + /// Import from file on first launch (if no prompts exist) + pub fn import_on_first_launch(db: &DbConnection, app_type: &str) -> Result { + let conn = db.lock().map_err(|e| e.to_string())?; + + // Check if prompts already exist + let existing = PromptDao::get_all(&conn, app_type).map_err(|e| e.to_string())?; + if !existing.is_empty() { + return Ok(0); + } + + // Read from live file + let app = app_type.parse::().map_err(|e| e.to_string())?; + let content = match prompt_sync::read_live_prompt(&app) { + Ok(Some(c)) if !c.trim().is_empty() => c, + _ => return Ok(0), + }; + + tracing::info!("Found prompt file, auto-importing for {}", app_type); + + let timestamp = chrono::Utc::now().timestamp(); + let prompt = Prompt { + id: format!("auto-imported-{}", timestamp), + app_type: app_type.to_string(), + name: format!( + "Auto-imported {}", + chrono::Local::now().format("%Y-%m-%d %H:%M") + ), + content, + description: Some("Automatically imported on first launch".to_string()), + enabled: true, // Enable on first import + created_at: Some(timestamp), + updated_at: Some(timestamp), + }; + + PromptDao::insert(&conn, &prompt).map_err(|e| e.to_string())?; + tracing::info!("Auto-imported prompt for {}", app_type); + + Ok(1) + } + + // Legacy method for compatibility + pub fn set_current(db: &DbConnection, app_type: &str, id: &str) -> Result<(), String> { + Self::enable(db, app_type, id) + } +} diff --git a/src-tauri/src/services/prompt_sync.rs b/src-tauri/src/services/prompt_sync.rs new file mode 100644 index 000000000..804c3b3db --- /dev/null +++ b/src-tauri/src/services/prompt_sync.rs @@ -0,0 +1,78 @@ +use crate::models::AppType; +use std::fs; +use std::io::Write; +use std::path::PathBuf; + +/// Get the prompt file path for an app +/// Claude: ~/CLAUDE.md (user's home directory) +/// Codex: ~/AGENTS.md +/// Gemini: ~/GEMINI.md +pub fn get_prompt_file_path(app: &AppType) -> Option { + let home = dirs::home_dir()?; + match app { + AppType::Claude => Some(home.join("CLAUDE.md")), + AppType::Codex => Some(home.join("AGENTS.md")), + AppType::Gemini => Some(home.join("GEMINI.md")), + AppType::ProxyCast => None, // ProxyCast doesn't have a prompt file + } +} + +/// Get the prompt file name for an app +pub fn get_prompt_filename(app: &AppType) -> &'static str { + match app { + AppType::Claude => "CLAUDE.md", + AppType::Codex => "AGENTS.md", + AppType::Gemini => "GEMINI.md", + AppType::ProxyCast => "", + } +} + +/// Read current content from the live prompt file +pub fn read_live_prompt(app: &AppType) -> Result, String> { + let path = get_prompt_file_path(app).ok_or("Cannot determine prompt file path")?; + + if !path.exists() { + return Ok(None); + } + + fs::read_to_string(&path) + .map(|c| Some(c)) + .map_err(|e| format!("Failed to read prompt file: {}", e)) +} + +/// Write content to the live prompt file (atomic write) +pub fn write_live_prompt(app: &AppType, content: &str) -> Result<(), String> { + let path = get_prompt_file_path(app).ok_or("Cannot determine prompt file path")?; + + // Ensure parent directory exists + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|e| format!("Failed to create directory: {}", e))?; + } + + // Atomic write: write to temp file first, then rename + let temp_path = path.with_extension("md.tmp"); + + let mut file = + fs::File::create(&temp_path).map_err(|e| format!("Failed to create temp file: {}", e))?; + + file.write_all(content.as_bytes()) + .map_err(|e| format!("Failed to write content: {}", e))?; + + file.sync_all() + .map_err(|e| format!("Failed to sync file: {}", e))?; + + fs::rename(&temp_path, &path).map_err(|e| format!("Failed to rename file: {}", e))?; + + Ok(()) +} + +/// Delete the live prompt file +pub fn delete_live_prompt(app: &AppType) -> Result<(), String> { + let path = get_prompt_file_path(app).ok_or("Cannot determine prompt file path")?; + + if path.exists() { + fs::remove_file(&path).map_err(|e| format!("Failed to delete prompt file: {}", e))?; + } + + Ok(()) +} diff --git a/src-tauri/src/services/switch.rs b/src-tauri/src/services/switch.rs new file mode 100644 index 000000000..decbe2f58 --- /dev/null +++ b/src-tauri/src/services/switch.rs @@ -0,0 +1,169 @@ +use crate::database::dao::providers::ProviderDao; +use crate::database::DbConnection; +use crate::models::{AppType, Provider}; +use crate::services::live_sync; + +pub struct SwitchService; + +impl SwitchService { + pub fn get_providers(db: &DbConnection, app_type: &str) -> Result, String> { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderDao::get_all(&conn, app_type).map_err(|e| e.to_string()) + } + + pub fn get_current_provider( + db: &DbConnection, + app_type: &str, + ) -> Result, String> { + let conn = db.lock().map_err(|e| e.to_string())?; + ProviderDao::get_current(&conn, app_type).map_err(|e| e.to_string()) + } + + pub fn add_provider(db: &DbConnection, provider: Provider) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + + // Check if this is the first provider for this app type + let existing = ProviderDao::get_all(&conn, &provider.app_type).map_err(|e| e.to_string())?; + let is_first = existing.is_empty(); + + ProviderDao::insert(&conn, &provider).map_err(|e| e.to_string())?; + + // If this is the first provider, automatically set it as current and sync + if is_first { + ProviderDao::set_current(&conn, &provider.app_type, &provider.id) + .map_err(|e| e.to_string())?; + + if let Ok(app_type_enum) = provider.app_type.parse::() { + if app_type_enum != AppType::ProxyCast { + live_sync::sync_to_live(&app_type_enum, &provider) + .map_err(|e| format!("Failed to sync: {}", e))?; + } + } + } + + Ok(()) + } + + pub fn update_provider(db: &DbConnection, provider: Provider) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + + // Check if this is the current provider + let current = ProviderDao::get_current(&conn, &provider.app_type).map_err(|e| e.to_string())?; + let is_current = current.as_ref().map(|p| p.id == provider.id).unwrap_or(false); + + ProviderDao::update(&conn, &provider).map_err(|e| e.to_string())?; + + // If this is the current provider, sync to live + if is_current { + if let Ok(app_type_enum) = provider.app_type.parse::() { + if app_type_enum != AppType::ProxyCast { + live_sync::sync_to_live(&app_type_enum, &provider) + .map_err(|e| format!("Failed to sync: {}", e))?; + } + } + } + + Ok(()) + } + + pub fn delete_provider(db: &DbConnection, app_type: &str, id: &str) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + + // Check if trying to delete the current provider + let current = ProviderDao::get_current(&conn, app_type).map_err(|e| e.to_string())?; + if let Some(ref current_provider) = current { + if current_provider.id == id { + return Err("Cannot delete the currently active provider".to_string()); + } + } + + ProviderDao::delete(&conn, app_type, id).map_err(|e| e.to_string()) + } + + pub fn switch_provider(db: &DbConnection, app_type: &str, id: &str) -> Result<(), String> { + let conn = db.lock().map_err(|e| e.to_string())?; + + // Get target provider + let target_provider = ProviderDao::get_by_id(&conn, app_type, id) + .map_err(|e| e.to_string())? + .ok_or_else(|| format!("Provider not found: {}", id))?; + + let app_type_enum = app_type.parse::().map_err(|e| e.to_string())?; + + // Skip backfill and sync for ProxyCast + if app_type_enum != AppType::ProxyCast { + // Backfill: Read current live config and save to current provider + if let Some(current_provider) = ProviderDao::get_current(&conn, app_type) + .map_err(|e| e.to_string())? + { + // Only backfill if switching to a different provider + if current_provider.id != id { + if let Ok(live_settings) = live_sync::read_live_settings(&app_type_enum) { + // Update current provider with live settings + let mut updated_provider = current_provider.clone(); + updated_provider.settings_config = live_settings; + let _ = ProviderDao::update(&conn, &updated_provider); + } + } + } + } + + // Set new current provider + ProviderDao::set_current(&conn, app_type, id).map_err(|e| e.to_string())?; + + // Sync target provider to live config + if app_type_enum != AppType::ProxyCast { + live_sync::sync_to_live(&app_type_enum, &target_provider) + .map_err(|e| format!("Failed to sync: {}", e))?; + } + + Ok(()) + } + + /// Import current live config as a default provider + pub fn import_default_config(db: &DbConnection, app_type: &str) -> Result { + let conn = db.lock().map_err(|e| e.to_string())?; + + // Check if providers already exist + let existing = ProviderDao::get_all(&conn, app_type).map_err(|e| e.to_string())?; + if !existing.is_empty() { + return Ok(false); // Already has providers, skip import + } + + let app_type_enum = app_type.parse::().map_err(|e| e.to_string())?; + + // Skip for ProxyCast + if app_type_enum == AppType::ProxyCast { + return Ok(false); + } + + // Read live settings + let live_settings = live_sync::read_live_settings(&app_type_enum) + .map_err(|e| format!("Failed to read live settings: {}", e))?; + + // Create default provider + let provider = Provider { + id: "default".to_string(), + app_type: app_type.to_string(), + name: "Default (Imported)".to_string(), + settings_config: live_settings, + category: Some("custom".to_string()), + icon: None, + icon_color: Some("#6366f1".to_string()), + notes: Some("Imported from existing configuration".to_string()), + is_current: true, + sort_index: Some(0), + created_at: Some(chrono::Utc::now().timestamp()), + }; + + ProviderDao::insert(&conn, &provider).map_err(|e| e.to_string())?; + + Ok(true) + } + + /// Read current live settings for an app type + pub fn read_live_settings(app_type: &str) -> Result { + let app_type_enum = app_type.parse::().map_err(|e| e.to_string())?; + live_sync::read_live_settings(&app_type_enum).map_err(|e| e.to_string()) + } +} diff --git a/src/App.tsx b/src/App.tsx index 9b29a3c22..c9e38a14a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,11 +2,24 @@ import { useState } from "react"; import { Sidebar } from "./components/Sidebar"; import { Dashboard } from "./components/Dashboard"; import { Providers } from "./components/Providers"; -import { Models } from "./components/Models"; -import { Settings } from "./components/Settings"; -import { Logs } from "./components/Logs"; +import { SettingsPage } from "./components/settings"; +import { SwitchPage } from "./components/switch"; +import { ClientsPage } from "./components/clients"; +import { McpPage } from "./components/mcp"; +import { PromptsPage } from "./components/prompts"; +import { CredentialsPage } from "./components/credentials/CredentialsPage"; +import { ApiServerPage } from "./components/api-server/ApiServerPage"; -type Page = "dashboard" | "providers" | "models" | "settings" | "logs"; +type Page = + | "dashboard" + | "credentials" + | "clients" + | "api-server" + | "providers" + | "settings" + | "switch" + | "mcp" + | "prompts"; function App() { const [currentPage, setCurrentPage] = useState("dashboard"); @@ -15,14 +28,22 @@ function App() { switch (currentPage) { case "dashboard": return ; + case "credentials": + return ; + case "clients": + return ; + case "api-server": + return ; case "providers": return ; - case "models": - return ; case "settings": - return ; - case "logs": - return ; + return ; + case "switch": + return ; + case "mcp": + return ; + case "prompts": + return ; default: return ; } diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 0a91748b6..af6f954e0 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -1,145 +1,53 @@ -import { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect } from "react"; import { Activity, Server, Zap, Clock, - Play, - Copy, - Check, - ChevronDown, - ChevronUp, - RefreshCw, + Key, + Monitor, + Globe, + CheckCircle2, + AlertCircle, } from "lucide-react"; import { - startServer, - stopServer, getServerStatus, getConfig, - reloadCredentials, - testApi, ServerStatus, Config, - TestResult, - getKiroCredentials, - checkAndReloadCredentials, - getTokenFileHash, - KiroCredentialStatus, + getDefaultProvider, } from "@/hooks/useTauri"; - -interface TestState { - endpoint: string; - status: "idle" | "loading" | "success" | "error"; - response?: string; - time?: number; - httpStatus?: number; -} +import { useAllOAuthCredentials } from "@/hooks/useOAuthCredentials"; export function Dashboard() { const [status, setStatus] = useState(null); const [config, setConfig] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [testResults, setTestResults] = useState>({}); - const [copiedCmd, setCopiedCmd] = useState(null); - const [expandedTest, setExpandedTest] = useState(null); - - // Token sync state - const [kiroStatus, setKiroStatus] = useState( - null, - ); - const [lastSyncTime, setLastSyncTime] = useState(null); - const [lastCheckTime, setLastCheckTime] = useState(null); - const kiroHashRef = useRef(""); - - const fetchStatus = async () => { - try { - const s = await getServerStatus(); - setStatus(s); - } catch (e) { - console.error(e); - } - }; - - const fetchConfig = async () => { - try { - const c = await getConfig(); - setConfig(c); - } catch (e) { - console.error(e); - } - }; + const [defaultProvider, setDefaultProvider] = useState("kiro"); + const { credentials: oauthCredentials, reload: reloadCredentials } = + useAllOAuthCredentials(); useEffect(() => { - fetchStatus(); - fetchConfig(); - loadKiroStatus(); - initTokenHash(); - - const statusInterval = setInterval(fetchStatus, 3000); - const tokenInterval = setInterval(checkTokenFileChanges, 10000); - - return () => { - clearInterval(statusInterval); - clearInterval(tokenInterval); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const initTokenHash = async () => { - try { - kiroHashRef.current = await getTokenFileHash(); - } catch (e) { - console.error("Failed to get initial hash:", e); - } - }; - - const loadKiroStatus = async () => { - try { - const status = await getKiroCredentials(); - setKiroStatus(status); - } catch (e) { - console.error("Failed to load Kiro status:", e); - } - }; - - const checkTokenFileChanges = async () => { - setLastCheckTime(new Date()); - try { - const result = await checkAndReloadCredentials(kiroHashRef.current); - kiroHashRef.current = result.new_hash; - if (result.changed && result.reloaded) { - await loadKiroStatus(); - setLastSyncTime(new Date()); + const fetchData = async () => { + try { + const [s, c, dp] = await Promise.all([ + getServerStatus(), + getConfig(), + getDefaultProvider(), + ]); + setStatus(s); + setConfig(c); + setDefaultProvider(dp); + } catch (e) { + console.error("Failed to fetch data:", e); } - } catch (e) { - console.error("Token check error:", e); - } - }; + }; - const handleStart = async () => { - setLoading(true); - setError(null); - try { - await reloadCredentials(); - await startServer(); - await fetchStatus(); - } catch (e: any) { - setError(e.toString()); - } - setLoading(false); - }; + fetchData(); + reloadCredentials(); - const handleStop = async () => { - setLoading(true); - try { - await stopServer(); - await fetchStatus(); - } catch (e: any) { - setError(e.toString()); - } - setLoading(false); - }; + const interval = setInterval(fetchData, 5000); + return () => clearInterval(interval); + }, [reloadCredentials]); const formatUptime = (secs: number) => { const h = Math.floor(secs / 3600); @@ -150,152 +58,37 @@ export function Dashboard() { const serverUrl = status ? `http://${status.host}:${status.port}` : "http://localhost:3001"; - const apiKey = config?.server.api_key || "proxycast-key"; - const maskedKey = - apiKey.length > 8 ? apiKey.slice(0, 4) + "****" + apiKey.slice(-4) : "****"; - // 测试端点配置 - const testEndpoints = [ - { - id: "health", - name: "健康检查", - method: "GET", - path: "/health", - needsAuth: false, - body: null, - }, - { - id: "models", - name: "模型列表", - method: "GET", - path: "/v1/models", - needsAuth: true, - body: null, - }, - { - id: "chat", - name: "OpenAI 聊天", - method: "POST", - path: "/v1/chat/completions", - needsAuth: true, - body: JSON.stringify({ - model: "claude-sonnet-4-5", - messages: [{ role: "user", content: "Say hi in one word" }], - }), - }, - { - id: "anthropic", - name: "Anthropic 消息", - method: "POST", - path: "/v1/messages", - needsAuth: true, - body: JSON.stringify({ - model: "claude-sonnet-4-5", - max_tokens: 100, - messages: [ - { - role: "user", - content: "What is 1+1? Answer with just the number.", - }, - ], - }), - }, - ]; - - const runTest = async (endpoint: (typeof testEndpoints)[0]) => { - setTestResults((prev) => ({ - ...prev, - [endpoint.id]: { endpoint: endpoint.path, status: "loading" }, - })); - - try { - const result: TestResult = await testApi( - endpoint.method, - endpoint.path, - endpoint.body, - endpoint.needsAuth, // maps to 'auth' parameter - ); - - // 添加调试日志 - console.log(`Test ${endpoint.id}:`, result); - - setTestResults((prev) => ({ - ...prev, - [endpoint.id]: { - endpoint: endpoint.path, - status: result.success ? "success" : "error", - response: result.body || `HTTP ${result.status}: 无响应内容`, - time: result.time_ms, - httpStatus: result.status, - }, - })); - } catch (e: any) { - console.error(`Test ${endpoint.id} error:`, e); - setTestResults((prev) => ({ - ...prev, - [endpoint.id]: { - endpoint: endpoint.path, - status: "error", - response: `请求失败: ${e.toString()}`, - }, - })); + const getProviderName = (id: string) => { + switch (id) { + case "kiro": + return "Kiro Claude"; + case "gemini": + return "Gemini CLI"; + case "qwen": + return "通义千问"; + case "openai": + return "OpenAI 自定义"; + case "claude": + return "Claude 自定义"; + default: + return id; } }; - const runAllTests = async () => { - for (const endpoint of testEndpoints) { - await runTest(endpoint); - } - }; - - const getCurlCommand = (endpoint: (typeof testEndpoints)[0]) => { - let cmd = `curl -s ${serverUrl}${endpoint.path}`; - if (endpoint.needsAuth) { - cmd += ` \\\n -H "Authorization: Bearer ${apiKey}"`; - } - if (endpoint.body) { - cmd += ` \\\n -H "Content-Type: application/json"`; - cmd += ` \\\n -d '${endpoint.body}'`; - } - return cmd; - }; - - const copyCommand = (id: string, cmd: string) => { - navigator.clipboard.writeText(cmd); - setCopiedCmd(id); - setTimeout(() => setCopiedCmd(null), 2000); - }; - - const getStatusBadge = (result?: TestState) => { - if (!result || result.status === "idle") { - return 未测试; - } - if (result.status === "loading") { - return 测试中...; - } - if (result.status === "success") { - return ✓ {result.time}ms; - } - return ( - - ✗ 失败 {result.httpStatus ? `(${result.httpStatus})` : ""} - - ); - }; - return (

仪表盘

-

服务状态概览

+

系统状态概览

- {/* Status Cards */} + {/* Server Status Cards */}
- 状态 + 服务状态
- 当前 Provider + 默认 Provider
-
Kiro
+
{getProviderName(defaultProvider)}
- {/* Token 同步状态 */} -
-
-

- - Token 同步状态 -

-
- - 自动监测中 -
-
-
-
- Token 状态: - + c.is_valid).length > 0 + ? "success" + : "warning" + } + statusText={`${oauthCredentials.filter((c) => c.is_valid).length}/${oauthCredentials.length} 有效`} + /> + + +
+ + {/* OAuth Credentials Overview */} +
+

+ + OAuth 凭证状态 +

+
+ {oauthCredentials.map((cred) => ( +
- {kiroStatus?.has_access_token ? "✓ 已加载" : "✗ 未加载"} - -
-
- Refresh Token: - - {kiroStatus?.has_refresh_token ? "✓ 可用" : "✗ 不可用"} - -
-
- 最后同步: - - {lastSyncTime ? lastSyncTime.toLocaleTimeString() : "从未同步"} - -
-
- 最后检测: - - {lastCheckTime ? lastCheckTime.toLocaleTimeString() : "-"} - -
+
+
{getProviderName(cred.provider)}
+
+ {cred.has_access_token ? "Token 已加载" : "未配置"} +
+
+
+
+ ))}
- {error && ( -
- {error} + {/* Server Info */} + {config && ( +
+

服务器信息

+
+
+ API 地址: + {serverUrl} +
+
+ API Key: + + {config.server.api_key.length > 8 + ? `${config.server.api_key.slice(0, 4)}****${config.server.api_key.slice(-4)}` + : "****"} + +
+
)} +
+ ); +} - {/* Server Control */} -
-

服务控制

-
- - +function QuickLinkCard({ + icon: Icon, + title, + description, + status, + statusText, +}: { + icon: React.ElementType; + title: string; + description: string; + status: "success" | "warning" | "error" | "info"; + statusText: string; +}) { + const statusColors = { + success: "text-green-600", + warning: "text-yellow-600", + error: "text-red-600", + info: "text-blue-600", + }; + + const StatusIcon = status === "success" ? CheckCircle2 : AlertCircle; + + return ( +
+
+
+
-
- - API 地址:{" "} - {serverUrl} - - - API Key:{" "} - {maskedKey} - +
+

{title}

+

{description}

- - {/* API 测试 */} -
-
-
-

API 测试

-

- 如遇问题,请打开浏览器开发者工具查看控制台日志 -

-
- -
- -
- {testEndpoints.map((endpoint) => { - const result = testResults[endpoint.id]; - const isExpanded = expandedTest === endpoint.id; - const curlCmd = getCurlCommand(endpoint); - - return ( -
-
-
- - {endpoint.method} - - {endpoint.name} - - {endpoint.path} - - {getStatusBadge(result)} -
-
- - - -
-
- - {isExpanded && ( -
-
-

- curl 命令 -

-
-                        {curlCmd}
-                      
-
- {result?.response && ( -
-

- 响应{" "} - {result.httpStatus && `(HTTP ${result.httpStatus})`} -

-
-                          {(() => {
-                            try {
-                              return JSON.stringify(
-                                JSON.parse(result.response),
-                                null,
-                                2,
-                              );
-                            } catch {
-                              return result.response || "(空响应)";
-                            }
-                          })()}
-                        
-
- )} -
- )} -
- ); - })} -
+
+ + {statusText}
); diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx deleted file mode 100644 index f70070d7c..000000000 --- a/src/components/Settings.tsx +++ /dev/null @@ -1,380 +0,0 @@ -import { useState, useEffect } from "react"; -import { - Eye, - EyeOff, - Copy, - Check, - Shield, - AlertTriangle, - CheckCircle2, - XCircle, - Loader2, -} from "lucide-react"; -import { - getConfig, - saveConfig, - Config, - checkApiCompatibility, - ApiCompatibilityResult, -} from "@/hooks/useTauri"; - -export function Settings() { - const [config, setConfig] = useState(null); - const [showApiKey, setShowApiKey] = useState(false); - const [copied, setCopied] = useState(false); - const [saving, setSaving] = useState(false); - const [message, setMessage] = useState(null); - - // API Compatibility Check - const [checking, setChecking] = useState(false); - const [checkResult, setCheckResult] = useState( - null, - ); - const [lastCheckTime, setLastCheckTime] = useState(null); - - useEffect(() => { - loadConfig(); - }, []); - - const loadConfig = async () => { - try { - const c = await getConfig(); - setConfig(c); - } catch (e) { - console.error(e); - } - }; - - const handleSave = async () => { - if (!config) return; - setSaving(true); - setMessage(null); - try { - await saveConfig(config); - setMessage("设置已保存"); - setTimeout(() => setMessage(null), 2000); - } catch (e: any) { - setMessage(`保存失败: ${e.toString()}`); - } - setSaving(false); - }; - - const copyApiKey = () => { - if (config) { - navigator.clipboard.writeText(config.server.api_key); - setCopied(true); - setTimeout(() => setCopied(false), 2000); - } - }; - - const handleCheckApiCompatibility = async (provider: string) => { - setChecking(true); - setCheckResult(null); - try { - const result = await checkApiCompatibility(provider); - setCheckResult(result); - setLastCheckTime(new Date()); - } catch (e) { - setMessage(`API 检测失败: ${e}`); - setTimeout(() => setMessage(null), 5000); - } - setChecking(false); - }; - - const getStatusIcon = (status: string) => { - switch (status) { - case "healthy": - return ; - case "partial": - return ; - case "error": - return ; - default: - return null; - } - }; - - const getStatusText = (status: string) => { - switch (status) { - case "healthy": - return "所有模型可用"; - case "partial": - return "部分模型可用"; - case "error": - return "API 不可用"; - default: - return "未知"; - } - }; - - if (!config) { - return ( -
-
- - - - - 加载配置中... -
-
- ); - } - - return ( -
-
-

设置

-

配置服务参数

-
- - {message && ( -
- {message} -
- )} - -
-
- - - setConfig({ - ...config, - server: { ...config.server, host: e.target.value }, - }) - } - className="w-full rounded-lg border bg-background px-3 py-2 text-sm" - /> -
- -
- - - setConfig({ - ...config, - server: { - ...config.server, - port: parseInt(e.target.value) || 3001, - }, - }) - } - className="w-full rounded-lg border bg-background px-3 py-2 text-sm" - /> -
- -
- -
-
- - setConfig({ - ...config, - server: { ...config.server, api_key: e.target.value }, - }) - } - className="w-full rounded-lg border bg-background px-3 py-2 pr-20 text-sm" - /> -
- - -
-
-
-
- - -
- - {/* Claude Code 兼容性检测 */} -
-
- -

Claude Code 兼容性检测

-
-

- 检测 API 是否支持 Claude Code 所需的功能:基础对话、Tool Calls 等 -

- -
-

检测项目:

-
    -
  • 基础对话能力 (basic)
  • -
  • Tool Calls 支持 (tool_call) - Claude Code 核心功能
  • -
-
- -
- - - -
- - {lastCheckTime && ( -

- 最后检测时间: {lastCheckTime.toLocaleString()} -

- )} - - {checkResult && ( -
-
-
- {getStatusIcon(checkResult.overall_status)} - - {checkResult.provider.toUpperCase()} -{" "} - {getStatusText(checkResult.overall_status)} - -
- - {new Date(checkResult.checked_at).toLocaleString()} - -
- - {/* 检测结果 */} -
-

检测结果:

- {checkResult.results.map((r) => ( -
-
- {r.available ? ( - - ) : ( - - )} - - {r.model.includes("tool_call") ? ( - - {r.model} - - ) : ( - r.model - )} - -
-
- {r.status > 0 && HTTP {r.status}} - {r.time_ms}ms - {r.error_type && ( - - {r.error_type} - - )} -
-
- ))} -
- - {/* 警告信息 */} - {checkResult.warnings.length > 0 && ( -
-

警告:

- {checkResult.warnings.map((w, i) => ( -
- - {w} -
- ))} -
- )} -
- )} -
-
- ); -} diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 0b3ed41a6..143319c87 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,13 +1,24 @@ import { LayoutDashboard, - Server, Settings, - ScrollText, - Cpu, + Plug, + MessageSquare, + Key, + Monitor, + Globe, } from "lucide-react"; import { cn } from "@/lib/utils"; -type Page = "dashboard" | "providers" | "models" | "settings" | "logs"; +type Page = + | "dashboard" + | "credentials" + | "clients" + | "api-server" + | "providers" + | "settings" + | "switch" + | "mcp" + | "prompts"; interface SidebarProps { currentPage: Page; @@ -16,10 +27,15 @@ interface SidebarProps { const navItems = [ { id: "dashboard" as Page, label: "仪表盘", icon: LayoutDashboard }, - { id: "providers" as Page, label: "Provider", icon: Server }, - { id: "models" as Page, label: "模型", icon: Cpu }, - { id: "logs" as Page, label: "日志", icon: ScrollText }, + { id: "credentials" as Page, label: "凭证管理", icon: Key }, + { id: "clients" as Page, label: "AI Clients", icon: Monitor }, + { id: "api-server" as Page, label: "API Server", icon: Globe }, + { id: "mcp" as Page, label: "MCP", icon: Plug }, + { id: "prompts" as Page, label: "Prompts", icon: MessageSquare }, { id: "settings" as Page, label: "设置", icon: Settings }, + // Legacy pages (hidden but accessible) + // { id: "providers" as Page, label: "Provider (旧)", icon: Server }, + // { id: "switch" as Page, label: "Switch (旧)", icon: ArrowLeftRight }, ]; export function Sidebar({ currentPage, onNavigate }: SidebarProps) { diff --git a/src/components/api-server/ApiServerPage.tsx b/src/components/api-server/ApiServerPage.tsx new file mode 100644 index 000000000..d5f8dc337 --- /dev/null +++ b/src/components/api-server/ApiServerPage.tsx @@ -0,0 +1,796 @@ +import { useState, useEffect } from "react"; +import { + Activity, + Server, + Zap, + Clock, + Play, + Copy, + Check, + ChevronDown, + ChevronUp, + Settings, + RefreshCw, +} from "lucide-react"; +import { ModelsTab } from "./ModelsTab"; +import { LogsTab } from "./LogsTab"; +import { + startServer, + stopServer, + getServerStatus, + getConfig, + saveConfig, + reloadCredentials, + testApi, + ServerStatus, + Config, + TestResult, + getDefaultProvider, + setDefaultProvider, + // OpenAI/Claude Custom + getOpenAICustomStatus, + setOpenAICustomConfig, + getClaudeCustomStatus, + setClaudeCustomConfig, + OpenAICustomStatus, + ClaudeCustomStatus, +} from "@/hooks/useTauri"; + +interface TestState { + endpoint: string; + status: "idle" | "loading" | "success" | "error"; + response?: string; + time?: number; + httpStatus?: number; +} + +type TabId = "server" | "openai" | "claude" | "models" | "logs"; + +export function ApiServerPage() { + const [status, setStatus] = useState(null); + const [config, setConfig] = useState(null); + const [loading, setLoading] = useState(false); + const [_error, setError] = useState(null); + const [testResults, setTestResults] = useState>({}); + const [copiedCmd, setCopiedCmd] = useState(null); + const [expandedTest, setExpandedTest] = useState(null); + const [activeTab, setActiveTab] = useState("server"); + + // Config editing + const [editPort, setEditPort] = useState(""); + const [editApiKey, setEditApiKey] = useState(""); + const [defaultProvider, setDefaultProviderState] = useState("kiro"); + + // OpenAI Custom state + const [openaiStatus, setOpenaiStatus] = useState( + null, + ); + const [openaiApiKey, setOpenaiApiKey] = useState(""); + const [openaiBaseUrl, setOpenaiBaseUrl] = useState(""); + + // Claude Custom state + const [claudeStatus, setClaudeStatus] = useState( + null, + ); + const [claudeApiKey, setClaudeApiKey] = useState(""); + const [claudeBaseUrl, setClaudeBaseUrl] = useState(""); + + const [message, setMessage] = useState<{ + type: "success" | "error"; + text: string; + } | null>(null); + + const fetchStatus = async () => { + try { + const s = await getServerStatus(); + setStatus(s); + } catch (e) { + console.error(e); + } + }; + + const fetchConfig = async () => { + try { + const c = await getConfig(); + setConfig(c); + setEditPort(c.server.port.toString()); + setEditApiKey(c.server.api_key); + } catch (e) { + console.error(e); + } + }; + + const loadOpenAICustomStatus = async () => { + try { + const status = await getOpenAICustomStatus(); + setOpenaiStatus(status); + setOpenaiBaseUrl(status.base_url); + } catch (e) { + console.error("Failed to load OpenAI Custom status:", e); + } + }; + + const loadClaudeCustomStatus = async () => { + try { + const status = await getClaudeCustomStatus(); + setClaudeStatus(status); + setClaudeBaseUrl(status.base_url); + } catch (e) { + console.error("Failed to load Claude Custom status:", e); + } + }; + + useEffect(() => { + fetchStatus(); + fetchConfig(); + loadDefaultProvider(); + loadOpenAICustomStatus(); + loadClaudeCustomStatus(); + + const statusInterval = setInterval(fetchStatus, 3000); + return () => clearInterval(statusInterval); + }, []); + + const loadDefaultProvider = async () => { + try { + const dp = await getDefaultProvider(); + setDefaultProviderState(dp); + } catch (e) { + console.error("Failed to get default provider:", e); + } + }; + + const handleStart = async () => { + setLoading(true); + setError(null); + try { + await reloadCredentials(); + await startServer(); + await fetchStatus(); + setMessage({ type: "success", text: "服务已启动" }); + } catch (e: unknown) { + const errMsg = e instanceof Error ? e.message : String(e); + setError(errMsg); + setMessage({ type: "error", text: `启动失败: ${errMsg}` }); + } + setLoading(false); + }; + + const handleStop = async () => { + setLoading(true); + try { + await stopServer(); + await fetchStatus(); + setMessage({ type: "success", text: "服务已停止" }); + } catch (e: unknown) { + const errMsg = e instanceof Error ? e.message : String(e); + setError(errMsg); + setMessage({ type: "error", text: `停止失败: ${errMsg}` }); + } + setLoading(false); + }; + + const handleSaveServerConfig = async () => { + if (!config) return; + setLoading(true); + try { + const newConfig = { + ...config, + server: { + ...config.server, + port: parseInt(editPort) || 3001, + api_key: editApiKey, + }, + }; + await saveConfig(newConfig); + await fetchConfig(); + setMessage({ type: "success", text: "服务器配置已保存" }); + } catch (e: unknown) { + const errMsg = e instanceof Error ? e.message : String(e); + setMessage({ type: "error", text: `保存失败: ${errMsg}` }); + } + setLoading(false); + }; + + const handleSetDefaultProvider = async (providerId: string) => { + setLoading(true); + try { + await setDefaultProvider(providerId); + setDefaultProviderState(providerId); + setMessage({ type: "success", text: `默认 Provider 已切换为: ${providerId}` }); + } catch (e: unknown) { + const errMsg = e instanceof Error ? e.message : String(e); + setMessage({ type: "error", text: `切换失败: ${errMsg}` }); + } + setLoading(false); + }; + + const handleSaveOpenAIConfig = async () => { + setLoading(true); + try { + await setOpenAICustomConfig( + openaiApiKey || null, + openaiBaseUrl || null, + true, + ); + await loadOpenAICustomStatus(); + setMessage({ type: "success", text: "OpenAI 配置已保存" }); + } catch (e: unknown) { + const errMsg = e instanceof Error ? e.message : String(e); + setMessage({ type: "error", text: `保存失败: ${errMsg}` }); + } + setLoading(false); + }; + + const handleSaveClaudeConfig = async () => { + setLoading(true); + try { + await setClaudeCustomConfig( + claudeApiKey || null, + claudeBaseUrl || null, + true, + ); + await loadClaudeCustomStatus(); + setMessage({ type: "success", text: "Claude 配置已保存" }); + } catch (e: unknown) { + const errMsg = e instanceof Error ? e.message : String(e); + setMessage({ type: "error", text: `保存失败: ${errMsg}` }); + } + setLoading(false); + }; + + const formatUptime = (secs: number) => { + const h = Math.floor(secs / 3600); + const m = Math.floor((secs % 3600) / 60); + return `${h}h ${m}m`; + }; + + const serverUrl = status + ? `http://${status.host}:${status.port}` + : "http://localhost:3001"; + const apiKey = config?.server.api_key || "proxycast-key"; + + // Test endpoints + const testEndpoints = [ + { + id: "health", + name: "健康检查", + method: "GET", + path: "/health", + needsAuth: false, + body: null, + }, + { + id: "models", + name: "模型列表", + method: "GET", + path: "/v1/models", + needsAuth: true, + body: null, + }, + { + id: "chat", + name: "OpenAI Chat", + method: "POST", + path: "/v1/chat/completions", + needsAuth: true, + body: JSON.stringify({ + model: "claude-sonnet-4-5", + messages: [{ role: "user", content: "Say hi in one word" }], + }), + }, + { + id: "anthropic", + name: "Anthropic Messages", + method: "POST", + path: "/v1/messages", + needsAuth: true, + body: JSON.stringify({ + model: "claude-sonnet-4-5", + max_tokens: 100, + messages: [{ role: "user", content: "What is 1+1? Answer with just the number." }], + }), + }, + ]; + + const runTest = async (endpoint: (typeof testEndpoints)[0]) => { + setTestResults((prev) => ({ + ...prev, + [endpoint.id]: { endpoint: endpoint.path, status: "loading" }, + })); + + try { + const result: TestResult = await testApi( + endpoint.method, + endpoint.path, + endpoint.body, + endpoint.needsAuth, + ); + + setTestResults((prev) => ({ + ...prev, + [endpoint.id]: { + endpoint: endpoint.path, + status: result.success ? "success" : "error", + response: result.body || `HTTP ${result.status}: 无响应内容`, + time: result.time_ms, + httpStatus: result.status, + }, + })); + } catch (e: unknown) { + const errMsg = e instanceof Error ? e.message : String(e); + setTestResults((prev) => ({ + ...prev, + [endpoint.id]: { + endpoint: endpoint.path, + status: "error", + response: `请求失败: ${errMsg}`, + }, + })); + } + }; + + const runAllTests = async () => { + for (const endpoint of testEndpoints) { + await runTest(endpoint); + } + }; + + const getCurlCommand = (endpoint: (typeof testEndpoints)[0]) => { + let cmd = `curl -s ${serverUrl}${endpoint.path}`; + if (endpoint.needsAuth) { + cmd += ` \\\n -H "Authorization: Bearer ${apiKey}"`; + } + if (endpoint.body) { + cmd += ` \\\n -H "Content-Type: application/json"`; + cmd += ` \\\n -d '${endpoint.body}'`; + } + return cmd; + }; + + const copyCommand = (id: string, cmd: string) => { + navigator.clipboard.writeText(cmd); + setCopiedCmd(id); + setTimeout(() => setCopiedCmd(null), 2000); + }; + + const getStatusBadge = (result?: TestState) => { + if (!result || result.status === "idle") { + return 未测试; + } + if (result.status === "loading") { + return 测试中...; + } + if (result.status === "success") { + return {result.time}ms; + } + return ( + + 失败 {result.httpStatus ? `(${result.httpStatus})` : ""} + + ); + }; + + return ( +
+
+

API Server

+

+ 管理代理 API 服务器和自定义 API 配置 +

+
+ + {message && ( +
+ {message.type === "success" ? ( + + ) : ( + + )} + {message.text} +
+ )} + + {/* Status Cards */} +
+
+
+ + 状态 +
+
+
+ + {status?.running ? "运行中" : "已停止"} + +
+
+ +
+
+ + 请求数 +
+
{status?.requests || 0}
+
+ +
+
+ + 运行时间 +
+
+ {formatUptime(status?.uptime_secs || 0)} +
+
+ +
+
+ + 默认 Provider +
+
{defaultProvider}
+
+
+ + {/* Tabs */} +
+ {[ + { id: "server" as TabId, name: "服务器控制" }, + { id: "openai" as TabId, name: "OpenAI 自定义" }, + { id: "claude" as TabId, name: "Claude 自定义" }, + { id: "models" as TabId, name: "可用模型" }, + { id: "logs" as TabId, name: "日志" }, + ].map((tab) => ( + + ))} +
+ + {/* Server Control Tab */} + {activeTab === "server" && ( +
+ {/* Server Control */} +
+

+ + 服务控制 +

+
+ + +
+ +
+
+ + setEditPort(e.target.value)} + className="w-full rounded-lg border bg-background px-3 py-2" + /> +
+
+ + setEditApiKey(e.target.value)} + className="w-full rounded-lg border bg-background px-3 py-2" + /> +
+
+ +
+
+ API 地址:{" "} + {serverUrl} +
+ +
+
+ + {/* Default Provider */} +
+

默认 Provider

+
+ {["kiro", "gemini", "qwen", "openai", "claude"].map((p) => ( + + ))} +
+
+ + {/* API Testing */} +
+
+

API 测试

+ +
+ +
+ {testEndpoints.map((endpoint) => { + const result = testResults[endpoint.id]; + const isExpanded = expandedTest === endpoint.id; + const curlCmd = getCurlCommand(endpoint); + + return ( +
+
+
+ + {endpoint.method} + + {endpoint.name} + + {endpoint.path} + + {getStatusBadge(result)} +
+
+ + + +
+
+ + {isExpanded && ( +
+
+

+ curl 命令 +

+
+                            {curlCmd}
+                          
+
+ {result?.response && ( +
+

+ 响应{" "} + {result.httpStatus && `(HTTP ${result.httpStatus})`} +

+
+                              {(() => {
+                                try {
+                                  return JSON.stringify(
+                                    JSON.parse(result.response),
+                                    null,
+                                    2,
+                                  );
+                                } catch {
+                                  return result.response || "(空响应)";
+                                }
+                              })()}
+                            
+
+ )} +
+ )} +
+ ); + })} +
+
+
+ )} + + {/* OpenAI Custom Tab */} + {activeTab === "openai" && ( +
+

OpenAI 自定义 API 配置

+

+ 配置自定义 OpenAI 兼容 API 端点,用于转发请求到其他 OpenAI 兼容服务 +

+
+
+ + setOpenaiApiKey(e.target.value)} + placeholder="sk-..." + className="w-full rounded-lg border bg-background px-3 py-2 text-sm" + /> +
+
+ + setOpenaiBaseUrl(e.target.value)} + placeholder="https://api.openai.com/v1" + className="w-full rounded-lg border bg-background px-3 py-2 text-sm" + /> +
+
+ 状态: + + {openaiStatus?.has_api_key ? "已配置" : "未配置"} + +
+ +
+
+ )} + + {/* Claude Custom Tab */} + {activeTab === "claude" && ( +
+

Claude 自定义 API 配置

+

+ 配置自定义 Claude API 端点,用于直接调用 Anthropic API +

+
+
+ + setClaudeApiKey(e.target.value)} + placeholder="sk-ant-..." + className="w-full rounded-lg border bg-background px-3 py-2 text-sm" + /> +
+
+ + setClaudeBaseUrl(e.target.value)} + placeholder="https://api.anthropic.com" + className="w-full rounded-lg border bg-background px-3 py-2 text-sm" + /> +
+
+ 状态: + + {claudeStatus?.has_api_key ? "已配置" : "未配置"} + +
+ +
+
+ )} + + {/* Models Tab */} + {activeTab === "models" && } + + {/* Logs Tab */} + {activeTab === "logs" && } +
+ ); +} diff --git a/src/components/Logs.tsx b/src/components/api-server/LogsTab.tsx similarity index 73% rename from src/components/Logs.tsx rename to src/components/api-server/LogsTab.tsx index abdf688d5..816f43d1a 100644 --- a/src/components/Logs.tsx +++ b/src/components/api-server/LogsTab.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from "react"; import { Trash2, Download } from "lucide-react"; import { getLogs, clearLogs, LogEntry } from "@/hooks/useTauri"; -export function Logs() { +export function LogsTab() { const [logs, setLogs] = useState([]); const [autoScroll, setAutoScroll] = useState(true); const logsEndRef = useRef(null); @@ -81,36 +81,30 @@ export function Logs() { return (
-
-
-

日志

-

查看请求和系统日志

-
-
- - - -
+
+ + +
diff --git a/src/components/Models.tsx b/src/components/api-server/ModelsTab.tsx similarity index 97% rename from src/components/Models.tsx rename to src/components/api-server/ModelsTab.tsx index 47eaba941..05dbfcc57 100644 --- a/src/components/Models.tsx +++ b/src/components/api-server/ModelsTab.tsx @@ -58,7 +58,7 @@ const MODEL_GROUPS: Record< }, }; -export function Models() { +export function ModelsTab() { const [models, setModels] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); @@ -144,11 +144,6 @@ export function Models() { return (
-
-

可用模型

-

查看当前可用的 AI 模型列表

-
- {error && (
{error} diff --git a/src/components/clients/AppTabs.tsx b/src/components/clients/AppTabs.tsx new file mode 100644 index 000000000..a50953f0b --- /dev/null +++ b/src/components/clients/AppTabs.tsx @@ -0,0 +1,35 @@ +import { cn } from "@/lib/utils"; +import { AppType } from "@/lib/api/switch"; + +interface AppTabsProps { + activeApp: AppType; + onAppChange: (app: AppType) => void; +} + +const apps: { id: AppType; label: string; description: string }[] = [ + { id: "claude", label: "Claude Code", description: "Claude CLI 配置" }, + { id: "codex", label: "Codex", description: "OpenAI Codex CLI" }, + { id: "gemini", label: "Gemini", description: "Google Gemini CLI" }, +]; + +export function AppTabs({ activeApp, onAppChange }: AppTabsProps) { + return ( +
+ {apps.map((app) => ( + + ))} +
+ ); +} diff --git a/src/components/clients/ClientsPage.tsx b/src/components/clients/ClientsPage.tsx new file mode 100644 index 000000000..2e389e6b0 --- /dev/null +++ b/src/components/clients/ClientsPage.tsx @@ -0,0 +1,22 @@ +import { useState } from "react"; +import { AppType } from "@/lib/api/switch"; +import { AppTabs } from "./AppTabs"; +import { ProviderList } from "./ProviderList"; + +export function ClientsPage() { + const [activeApp, setActiveApp] = useState("claude"); + + return ( +
+
+

AI Clients

+

+ 管理 Claude Code / Codex / Gemini CLI 的 Provider 配置 +

+
+ + + +
+ ); +} diff --git a/src/components/clients/LiveConfigModal.tsx b/src/components/clients/LiveConfigModal.tsx new file mode 100644 index 000000000..85067c3c6 --- /dev/null +++ b/src/components/clients/LiveConfigModal.tsx @@ -0,0 +1,90 @@ +import { useState, useEffect } from "react"; +import { X, RefreshCw, FileText } from "lucide-react"; +import { switchApi, AppType } from "@/lib/api/switch"; + +interface LiveConfigModalProps { + appType: AppType; + onClose: () => void; +} + +const configPaths: Record = { + claude: "~/.claude/settings.json", + codex: "~/.codex/auth.json & config.toml", + gemini: "~/.gemini/.env & settings.json", + proxycast: "", +}; + +export function LiveConfigModal({ appType, onClose }: LiveConfigModalProps) { + const [config, setConfig] = useState | null>(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const loadConfig = async () => { + setLoading(true); + setError(null); + try { + const data = await switchApi.readLiveSettings(appType); + setConfig(data); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + loadConfig(); + }, [appType]); + + return ( +
+
+
+
+ +

当前生效的配置

+
+
+ + +
+
+ +
+ {loading ? ( +
+ +
+ ) : error ? ( +
+

{error}

+
+ ) : config ? ( +
+              {JSON.stringify(config, null, 2)}
+            
+ ) : ( +

+ 无配置数据 +

+ )} +
+ +
+

+ 配置文件路径: {configPaths[appType]} +

+
+
+
+ ); +} diff --git a/src/components/clients/ProviderCard.tsx b/src/components/clients/ProviderCard.tsx new file mode 100644 index 000000000..3aa024bf0 --- /dev/null +++ b/src/components/clients/ProviderCard.tsx @@ -0,0 +1,103 @@ +import { Check, Edit2, Trash2, Zap } from "lucide-react"; +import { Provider } from "@/lib/api/switch"; +import { cn } from "@/lib/utils"; + +interface ProviderCardProps { + provider: Provider; + isCurrent: boolean; + onSwitch: () => void; + onEdit: () => void; + onDelete: () => void; +} + +export function ProviderCard({ + provider, + isCurrent, + onSwitch, + onEdit, + onDelete, +}: ProviderCardProps) { + return ( +
+ {isCurrent && ( +
+ +
+ )} + +
+
+
+ {provider.icon || provider.name.charAt(0).toUpperCase()} +
+
+

{provider.name}

+ {provider.category && ( + + {provider.category} + + )} +
+
+ +
+ + +
+
+ + {provider.notes && ( +

+ {provider.notes} +

+ )} + +
+ {isCurrent ? ( + 当前使用中 + ) : ( + + )} +
+
+ ); +} diff --git a/src/components/clients/ProviderForm.tsx b/src/components/clients/ProviderForm.tsx new file mode 100644 index 000000000..f4aeda1f9 --- /dev/null +++ b/src/components/clients/ProviderForm.tsx @@ -0,0 +1,883 @@ +import React, { useState, useMemo, useCallback } from "react"; +import { X, ExternalLink, Wand2, Eye, EyeOff } from "lucide-react"; +import { Provider, AppType } from "@/lib/api/switch"; +import { getConfig } from "@/hooks/useTauri"; +import { cn } from "@/lib/utils"; + +interface ProviderFormProps { + appType: AppType; + provider: Provider | null; + onSave: (data: Omit) => Promise; + onCancel: () => void; +} + +// 供应商分类 +type ProviderCategory = "official" | "cn_official" | "aggregator" | "third_party" | "proxy" | "custom"; + +// 预设供应商接口 +interface ProviderPreset { + id: string; + name: string; + category: ProviderCategory; + iconColor?: string; + websiteUrl?: string; + apiKeyUrl?: string; + // Claude 专属默认配置 + defaultBaseUrl?: string; + defaultModel?: string; + // Codex 专属默认配置 + defaultCodexAuth?: Record; + defaultCodexConfig?: string; + // Gemini 专属默认配置 + defaultGeminiEnv?: Record; +} + +// 预设供应商配置 +const presets: Record = { + claude: [ + // 官方 + { + id: "anthropic", + name: "Anthropic 官方", + category: "official", + iconColor: "#D97757", + websiteUrl: "https://www.anthropic.com/claude-code", + apiKeyUrl: "https://console.anthropic.com/settings/keys", + }, + // 国内官方 + { + id: "deepseek", + name: "DeepSeek", + category: "cn_official", + iconColor: "#0066FF", + websiteUrl: "https://platform.deepseek.com", + apiKeyUrl: "https://platform.deepseek.com/api_keys", + defaultBaseUrl: "https://api.deepseek.com/anthropic", + }, + { + id: "zhipu", + name: "智谱 GLM", + category: "cn_official", + iconColor: "#5B6BE8", + websiteUrl: "https://open.bigmodel.cn", + apiKeyUrl: "https://open.bigmodel.cn/usercenter/apikeys", + defaultBaseUrl: "https://open.bigmodel.cn/api/anthropic", + }, + { + id: "qwen", + name: "通义千问 Coder", + category: "cn_official", + iconColor: "#6B4FBB", + websiteUrl: "https://bailian.console.aliyun.com", + apiKeyUrl: "https://bailian.console.aliyun.com/?apiKey=1#/api-key", + defaultBaseUrl: "https://dashscope.aliyuncs.com/api/v2/apps/claude-code-proxy", + }, + { + id: "kimi", + name: "Kimi k2", + category: "cn_official", + iconColor: "#000000", + websiteUrl: "https://platform.moonshot.cn/console", + apiKeyUrl: "https://platform.moonshot.cn/console/api-keys", + defaultBaseUrl: "https://api.moonshot.cn/anthropic", + }, + { + id: "kimi-coding", + name: "Kimi For Coding", + category: "cn_official", + iconColor: "#000000", + websiteUrl: "https://www.kimi.com/coding/docs/", + apiKeyUrl: "https://www.kimi.com/coding/profile", + defaultBaseUrl: "https://api.kimi.com/coding/", + }, + { + id: "minimax", + name: "MiniMax", + category: "cn_official", + iconColor: "#F97316", + websiteUrl: "https://platform.minimaxi.com", + apiKeyUrl: "https://platform.minimaxi.com/user-center/basic-information/interface-key", + defaultBaseUrl: "https://api.minimaxi.com/anthropic", + }, + { + id: "doubao", + name: "豆包 Seed", + category: "cn_official", + iconColor: "#5DADEC", + websiteUrl: "https://www.volcengine.com/product/doubao", + apiKeyUrl: "https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey", + defaultBaseUrl: "https://ark.cn-beijing.volces.com/api/coding", + }, + // 聚合服务 + { + id: "openrouter", + name: "OpenRouter", + category: "aggregator", + iconColor: "#6366f1", + websiteUrl: "https://openrouter.ai", + apiKeyUrl: "https://openrouter.ai/keys", + defaultBaseUrl: "https://openrouter.ai/api", + }, + // 本地代理 + { + id: "proxycast", + name: "ProxyCast", + category: "proxy", + iconColor: "#3b82f6", + defaultBaseUrl: "http://127.0.0.1:3001", + }, + // 自定义 + { + id: "custom", + name: "自定义", + category: "custom", + iconColor: "#8b5cf6", + }, + ], + codex: [ + // 官方 + { + id: "openai", + name: "OpenAI 官方", + category: "official", + iconColor: "#10a37f", + websiteUrl: "https://chatgpt.com/codex", + apiKeyUrl: "https://platform.openai.com/api-keys", + }, + // 第三方 + { + id: "azure", + name: "Azure OpenAI", + category: "third_party", + iconColor: "#0078D4", + websiteUrl: "https://learn.microsoft.com/azure/ai-services/openai", + defaultCodexAuth: { + api_key: "", + api_base_url: "https://YOUR_RESOURCE_NAME.openai.azure.com/openai", + }, + defaultCodexConfig: `# Azure OpenAI 配置 +model = "gpt-4o" +`, + }, + // 本地代理 + { + id: "proxycast", + name: "ProxyCast", + category: "proxy", + iconColor: "#3b82f6", + defaultCodexAuth: { + api_key: "proxycast", + api_base_url: "http://127.0.0.1:3001/v1", + }, + }, + // 自定义 + { + id: "custom", + name: "自定义", + category: "custom", + iconColor: "#8b5cf6", + }, + ], + gemini: [ + // 官方 + { + id: "google", + name: "Google 官方", + category: "official", + iconColor: "#4285f4", + websiteUrl: "https://ai.google.dev/", + apiKeyUrl: "https://aistudio.google.com/app/apikey", + }, + // 本地代理 + { + id: "proxycast", + name: "ProxyCast", + category: "proxy", + iconColor: "#3b82f6", + defaultGeminiEnv: { + GEMINI_API_KEY: "proxycast", + GOOGLE_GEMINI_BASE_URL: "http://127.0.0.1:3001", + GEMINI_MODEL: "gemini-2.0-flash", + }, + }, + // 自定义 + { + id: "custom", + name: "自定义", + category: "custom", + iconColor: "#8b5cf6", + }, + ], + proxycast: [], +}; + +const categoryLabels: Record = { + official: "官方", + cn_official: "国内官方", + aggregator: "聚合服务", + third_party: "第三方", + proxy: "本地代理", + custom: "自定义", +}; + +// 默认 Codex auth.json 模板 +const defaultCodexAuth = JSON.stringify({ + api_key: "", + api_base_url: "" +}, null, 2); + +// 默认 Codex config.toml 模板 +const defaultCodexConfig = `# Codex 配置文件 +model = "gpt-4" +`; + +// 默认 Gemini .env 模板 +const defaultGeminiEnv = `GEMINI_API_KEY= +GOOGLE_GEMINI_BASE_URL= +GEMINI_MODEL=gemini-2.0-flash`; + +// 默认 Gemini settings.json 模板 +const defaultGeminiSettings = JSON.stringify({ + mcpServers: {} +}, null, 2); + +export function ProviderForm({ appType, provider, onSave, onCancel }: ProviderFormProps) { + const isEditMode = Boolean(provider); + const appPresets = useMemo(() => presets[appType] || [], [appType]); + + // 基础字段 + const [selectedPresetId, setSelectedPresetId] = useState( + isEditMode ? null : "custom" + ); + const [name, setName] = useState(provider?.name || ""); + const [notes, setNotes] = useState(provider?.notes || ""); + const [iconColor, setIconColor] = useState(provider?.icon_color || "#6366f1"); + + // 从 provider.settings_config 中提取 Claude 配置 + const extractClaudeConfig = () => { + if (!provider?.settings_config || typeof provider.settings_config !== 'object') { + return { apiKey: "", baseUrl: "", primaryModel: "claude-sonnet-4-20250514", haikuModel: "", sonnetModel: "", opusModel: "" }; + } + const env = (provider.settings_config as Record).env as Record | undefined; + if (!env) { + return { apiKey: "", baseUrl: "", primaryModel: "claude-sonnet-4-20250514", haikuModel: "", sonnetModel: "", opusModel: "" }; + } + return { + apiKey: env.ANTHROPIC_API_KEY || env.ANTHROPIC_AUTH_TOKEN || "", + baseUrl: env.ANTHROPIC_BASE_URL || "", + primaryModel: env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514", + haikuModel: env.ANTHROPIC_DEFAULT_HAIKU_MODEL || "", + sonnetModel: env.ANTHROPIC_DEFAULT_SONNET_MODEL || "", + opusModel: env.ANTHROPIC_DEFAULT_OPUS_MODEL || "", + }; + }; + + const claudeConfig = extractClaudeConfig(); + + // Claude 专属字段 - 编辑模式从现有配置加载 + const [apiKey, setApiKey] = useState(claudeConfig.apiKey); + const [baseUrl, setBaseUrl] = useState(claudeConfig.baseUrl); + const [primaryModel, setPrimaryModel] = useState(claudeConfig.primaryModel); + const [haikuModel, setHaikuModel] = useState(claudeConfig.haikuModel); + const [sonnetModel, setSonnetModel] = useState(claudeConfig.sonnetModel); + const [opusModel, setOpusModel] = useState(claudeConfig.opusModel); + + // Claude 配置 JSON(可编辑) + const [claudeConfigJson, setClaudeConfigJson] = useState(() => { + if (provider?.settings_config && typeof provider.settings_config === 'object') { + return JSON.stringify(provider.settings_config, null, 2); + } + return JSON.stringify({ env: {} }, null, 2); + }); + const [jsonError, setJsonError] = useState(null); + + // 当表单字段变化时,同步更新 JSON(仅当 JSON 未被手动修改时) + const [jsonManuallyEdited, setJsonManuallyEdited] = useState(false); + + // 从表单字段生成 JSON + const generateJsonFromFields = useCallback(() => { + const env: Record = {}; + if (apiKey) { + env.ANTHROPIC_AUTH_TOKEN = apiKey; + env.ANTHROPIC_API_KEY = apiKey; + } + if (baseUrl) env.ANTHROPIC_BASE_URL = baseUrl; + if (primaryModel) env.ANTHROPIC_MODEL = primaryModel; + if (haikuModel) env.ANTHROPIC_DEFAULT_HAIKU_MODEL = haikuModel; + if (sonnetModel) env.ANTHROPIC_DEFAULT_SONNET_MODEL = sonnetModel; + if (opusModel) env.ANTHROPIC_DEFAULT_OPUS_MODEL = opusModel; + return JSON.stringify({ env }, null, 2); + }, [apiKey, baseUrl, primaryModel, haikuModel, sonnetModel, opusModel]); + + // 同步表单到 JSON(当表单字段变化且 JSON 未被手动编辑时) + React.useEffect(() => { + if (!jsonManuallyEdited && appType === "claude") { + setClaudeConfigJson(generateJsonFromFields()); + } + }, [generateJsonFromFields, jsonManuallyEdited, appType]); + + // 处理 JSON 编辑 + const handleJsonChange = (value: string) => { + setClaudeConfigJson(value); + setJsonManuallyEdited(true); + setJsonError(null); + + // 尝试解析并同步回表单字段 + try { + const parsed = JSON.parse(value); + const env = parsed.env || {}; + setApiKey(env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY || ""); + setBaseUrl(env.ANTHROPIC_BASE_URL || ""); + setPrimaryModel(env.ANTHROPIC_MODEL || ""); + setHaikuModel(env.ANTHROPIC_DEFAULT_HAIKU_MODEL || ""); + setSonnetModel(env.ANTHROPIC_DEFAULT_SONNET_MODEL || ""); + setOpusModel(env.ANTHROPIC_DEFAULT_OPUS_MODEL || ""); + } catch { + setJsonError("JSON 格式错误"); + } + }; + + // 格式化 JSON + const handleFormatJson = () => { + try { + const parsed = JSON.parse(claudeConfigJson); + setClaudeConfigJson(JSON.stringify(parsed, null, 2)); + setJsonError(null); + } catch { + setJsonError("JSON 格式错误,无法格式化"); + } + }; + + // Codex 专属字段 - 使用代码编辑器 + const [codexAuth, setCodexAuth] = useState(() => { + if (provider?.settings_config && typeof provider.settings_config === 'object') { + const auth = (provider.settings_config as Record).auth; + if (auth) return JSON.stringify(auth, null, 2); + } + return defaultCodexAuth; + }); + const [codexConfig, setCodexConfig] = useState(() => { + if (provider?.settings_config && typeof provider.settings_config === 'object') { + const config = (provider.settings_config as Record).config; + if (typeof config === 'string') return config; + } + return defaultCodexConfig; + }); + + // Gemini 专属字段 - 使用代码编辑器 + const [geminiEnv, setGeminiEnv] = useState(() => { + if (provider?.settings_config && typeof provider.settings_config === 'object') { + const env = (provider.settings_config as Record).env; + if (env && typeof env === 'object') { + return Object.entries(env as Record) + .map(([k, v]) => `${k}=${v}`) + .join('\n'); + } + } + return defaultGeminiEnv; + }); + const [geminiSettings, setGeminiSettings] = useState(() => { + if (provider?.settings_config && typeof provider.settings_config === 'object') { + const config = (provider.settings_config as Record).config; + if (config && typeof config === 'object') { + return JSON.stringify(config, null, 2); + } + } + return defaultGeminiSettings; + }); + + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + const [showApiKey, setShowApiKey] = useState(false); + + // 当前选中的预设 + const selectedPreset = useMemo(() => { + return appPresets.find((p) => p.id === selectedPresetId); + }, [appPresets, selectedPresetId]); + + // 是否显示 API Key 和端点字段(编辑模式始终显示,新增模式非官方时显示) + const showApiFields = isEditMode || selectedPreset?.category !== "official"; + + // 按分类分组预设 + const groupedPresets = useMemo(() => { + const groups: Partial> = {}; + for (const preset of appPresets) { + const cat = preset.category; + if (!groups[cat]) groups[cat] = []; + groups[cat]!.push(preset); + } + return groups; + }, [appPresets]); + + const handlePresetChange = async (presetId: string) => { + setSelectedPresetId(presetId); + const preset = appPresets.find((p) => p.id === presetId); + if (preset) { + setName(preset.name); + setIconColor(preset.iconColor || "#6366f1"); + + // ProxyCast 预设:自动从设置中读取 API Key 和端口 + if (preset.id === "proxycast") { + try { + const config = await getConfig(); + const proxyApiKey = config.server.api_key || ""; + const proxyHost = config.server.host || "127.0.0.1"; + const proxyPort = config.server.port || 3001; + const proxyBaseUrl = `http://${proxyHost}:${proxyPort}`; + + if (appType === "claude") { + setApiKey(proxyApiKey); + setBaseUrl(proxyBaseUrl); + // 重置 JSON 手动编辑标记,让表单同步到 JSON + setJsonManuallyEdited(false); + } else if (appType === "codex") { + setCodexAuth(JSON.stringify({ + api_key: proxyApiKey, + api_base_url: `${proxyBaseUrl}/v1`, + }, null, 2)); + } else if (appType === "gemini") { + setGeminiEnv(`GEMINI_API_KEY=${proxyApiKey}\nGOOGLE_GEMINI_BASE_URL=${proxyBaseUrl}\nGEMINI_MODEL=gemini-2.0-flash`); + } + return; + } catch (e) { + console.error("Failed to load ProxyCast config:", e); + } + } + + // 应用预设的默认配置 + if (appType === "claude") { + // Claude: 设置默认 base URL + if (preset.defaultBaseUrl) { + setBaseUrl(preset.defaultBaseUrl); + } else { + setBaseUrl(""); + } + if (preset.defaultModel) { + setPrimaryModel(preset.defaultModel); + } + // 重置 JSON 手动编辑标记 + setJsonManuallyEdited(false); + } else if (appType === "codex") { + // Codex: 设置默认 auth.json + if (preset.defaultCodexAuth) { + setCodexAuth(JSON.stringify(preset.defaultCodexAuth, null, 2)); + } else { + setCodexAuth(defaultCodexAuth); + } + if (preset.defaultCodexConfig) { + setCodexConfig(preset.defaultCodexConfig); + } else { + setCodexConfig(defaultCodexConfig); + } + } else if (appType === "gemini") { + // Gemini: 设置默认 env + if (preset.defaultGeminiEnv) { + const envLines = Object.entries(preset.defaultGeminiEnv) + .map(([k, v]) => `${k}=${v}`) + .join('\n'); + setGeminiEnv(envLines); + } else { + setGeminiEnv(defaultGeminiEnv); + } + } + } + }; + + const buildSettingsConfig = (): Record => { + if (appType === "claude") { + // 直接使用 JSON 编辑器的值 + try { + return JSON.parse(claudeConfigJson); + } catch { + throw new Error("配置 JSON 格式错误"); + } + } + + if (appType === "codex") { + try { + const auth = JSON.parse(codexAuth); + return { auth, config: codexConfig }; + } catch { + throw new Error("auth.json 格式错误"); + } + } + + if (appType === "gemini") { + const env: Record = {}; + for (const line of geminiEnv.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const idx = trimmed.indexOf('='); + if (idx > 0) { + const key = trimmed.slice(0, idx).trim(); + const value = trimmed.slice(idx + 1).trim(); + env[key] = value; + } + } + try { + const config = JSON.parse(geminiSettings); + return { env, config }; + } catch { + throw new Error("settings.json 格式错误"); + } + } + + return {}; + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + if (!name.trim()) { + setError("请填写供应商名称"); + return; + } + + // 非官方供应商必填校验(新增模式必填,编辑模式可留空保持原值) + if (!isEditMode && showApiFields && appType === "claude" && !apiKey.trim()) { + setError("请填写 API Key"); + return; + } + + try { + setSaving(true); + const settings_config = buildSettingsConfig(); + await onSave({ + app_type: appType, + name: name.trim(), + settings_config, + category: selectedPreset?.category || "custom", + icon_color: iconColor, + notes: notes.trim() || undefined, + }); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + setSaving(false); + } + }; + + return ( +
+
+
+

+ {isEditMode ? "编辑供应商" : "添加供应商"} +

+ +
+ +
+ {/* 预设选择器(仅新增模式) */} + {!isEditMode && ( +
+ +
+ {Object.entries(groupedPresets).map(([category, items]) => ( +
+

+ {categoryLabels[category as ProviderCategory] || category} +

+
+ {items.map((preset) => ( + + ))} +
+
+ ))} +
+
+ )} + + {/* 基础字段 */} +
+
+ + setName(e.target.value)} + className="w-full px-3 py-2 rounded-lg border bg-background focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none" + placeholder="输入供应商名称" + required + /> +
+ +
+ + setNotes(e.target.value)} + className="w-full px-3 py-2 rounded-lg border bg-background focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none" + placeholder="可选备注信息" + /> +
+
+ + {/* Claude 表单字段 */} + {appType === "claude" && showApiFields && ( +
+
+
+ + {(selectedPreset?.apiKeyUrl || selectedPreset?.websiteUrl) && ( + + 获取 API Key + + + )} +
+
+ setApiKey(e.target.value)} + className="w-full px-3 py-2 pr-10 rounded-lg border bg-background focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none font-mono" + placeholder="sk-..." + /> + +
+
+ +
+ + setBaseUrl(e.target.value)} + className="w-full px-3 py-2 rounded-lg border bg-background focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none font-mono" + placeholder="https://api.example.com/v1" + /> +

留空使用默认端点

+
+ +
+ +
+
+ + setPrimaryModel(e.target.value)} + className="w-full px-3 py-2 rounded-lg border bg-background focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none text-sm" + placeholder="claude-sonnet-4-20250514" + /> +
+
+ + setHaikuModel(e.target.value)} + className="w-full px-3 py-2 rounded-lg border bg-background focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none text-sm" + /> +
+
+ + setSonnetModel(e.target.value)} + className="w-full px-3 py-2 rounded-lg border bg-background focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none text-sm" + /> +
+
+ + setOpusModel(e.target.value)} + className="w-full px-3 py-2 rounded-lg border bg-background focus:ring-2 focus:ring-primary/20 focus:border-primary outline-none text-sm" + /> +
+
+

+ 可选:指定默认使用的 Claude 模型,留空则使用系统默认。 +

+
+ + {/* 配置 JSON 编辑器 */} +
+
+ + +
+