diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock index 45bbcf71f..995d6b790 100644 --- a/desktop/src-tauri/Cargo.lock +++ b/desktop/src-tauri/Cargo.lock @@ -1028,6 +1028,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -1160,6 +1170,24 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" +[[package]] +name = "global-hotkey" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7" +dependencies = [ + "crossbeam-channel", + "keyboard-types", + "objc2", + "objc2-app-kit", + "once_cell", + "serde", + "thiserror 2.0.18", + "windows-sys 0.59.0", + "x11rb", + "xkeysym", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -1749,6 +1777,12 @@ dependencies = [ "libc", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.1" @@ -2769,6 +2803,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustversion" version = "1.0.22" @@ -3227,6 +3274,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-global-shortcut", "tauri-plugin-shell", "tracing", "tracing-subscriber", @@ -3545,6 +3593,21 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-global-shortcut" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "424af23c7e88d05e4a1a6fc2c7be077912f8c76bd7900fd50aa2b7cbf5a2c405" +dependencies = [ + "global-hotkey", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-plugin-shell" version = "2.3.5" @@ -5060,6 +5123,29 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + [[package]] name = "yoke" version = "0.8.1" diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml index 9c5943e05..477d4c2b2 100644 --- a/desktop/src-tauri/Cargo.toml +++ b/desktop/src-tauri/Cargo.toml @@ -9,6 +9,7 @@ path = "src/main.rs" [dependencies] tauri = { version = "2", features = ["macos-private-api"] } +tauri-plugin-global-shortcut = "2" tauri-plugin-shell = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/desktop/src-tauri/gen/schemas/acl-manifests.json b/desktop/src-tauri/gen/schemas/acl-manifests.json index 86cdb1f5f..96d86d645 100644 --- a/desktop/src-tauri/gen/schemas/acl-manifests.json +++ b/desktop/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 +{"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},"global-shortcut":{"default_permission":{"identifier":"default","description":"No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n","permissions":[]},"permissions":{"allow-is-registered":{"identifier":"allow-is-registered","description":"Enables the is_registered command without any pre-configured scope.","commands":{"allow":["is_registered"],"deny":[]}},"allow-register":{"identifier":"allow-register","description":"Enables the register command without any pre-configured scope.","commands":{"allow":["register"],"deny":[]}},"allow-register-all":{"identifier":"allow-register-all","description":"Enables the register_all command without any pre-configured scope.","commands":{"allow":["register_all"],"deny":[]}},"allow-unregister":{"identifier":"allow-unregister","description":"Enables the unregister command without any pre-configured scope.","commands":{"allow":["unregister"],"deny":[]}},"allow-unregister-all":{"identifier":"allow-unregister-all","description":"Enables the unregister_all command without any pre-configured scope.","commands":{"allow":["unregister_all"],"deny":[]}},"deny-is-registered":{"identifier":"deny-is-registered","description":"Denies the is_registered command without any pre-configured scope.","commands":{"allow":[],"deny":["is_registered"]}},"deny-register":{"identifier":"deny-register","description":"Denies the register command without any pre-configured scope.","commands":{"allow":[],"deny":["register"]}},"deny-register-all":{"identifier":"deny-register-all","description":"Denies the register_all command without any pre-configured scope.","commands":{"allow":[],"deny":["register_all"]}},"deny-unregister":{"identifier":"deny-unregister","description":"Denies the unregister command without any pre-configured scope.","commands":{"allow":[],"deny":["unregister"]}},"deny-unregister-all":{"identifier":"deny-unregister-all","description":"Denies the unregister_all command without any pre-configured scope.","commands":{"allow":[],"deny":["unregister_all"]}}},"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/desktop/src-tauri/gen/schemas/desktop-schema.json b/desktop/src-tauri/gen/schemas/desktop-schema.json index f827fe175..2cb2c2a88 100644 --- a/desktop/src-tauri/gen/schemas/desktop-schema.json +++ b/desktop/src-tauri/gen/schemas/desktop-schema.json @@ -2354,6 +2354,72 @@ "const": "core:window:deny-unminimize", "markdownDescription": "Denies the unminimize command without any pre-configured scope." }, + { + "description": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n", + "type": "string", + "const": "global-shortcut:default", + "markdownDescription": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n" + }, + { + "description": "Enables the is_registered command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-is-registered", + "markdownDescription": "Enables the is_registered command without any pre-configured scope." + }, + { + "description": "Enables the register command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-register", + "markdownDescription": "Enables the register command without any pre-configured scope." + }, + { + "description": "Enables the register_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-register-all", + "markdownDescription": "Enables the register_all command without any pre-configured scope." + }, + { + "description": "Enables the unregister command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-unregister", + "markdownDescription": "Enables the unregister command without any pre-configured scope." + }, + { + "description": "Enables the unregister_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-unregister-all", + "markdownDescription": "Enables the unregister_all command without any pre-configured scope." + }, + { + "description": "Denies the is_registered command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-is-registered", + "markdownDescription": "Denies the is_registered command without any pre-configured scope." + }, + { + "description": "Denies the register command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-register", + "markdownDescription": "Denies the register command without any pre-configured scope." + }, + { + "description": "Denies the register_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-register-all", + "markdownDescription": "Denies the register_all command without any pre-configured scope." + }, + { + "description": "Denies the unregister command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-unregister", + "markdownDescription": "Denies the unregister command without any pre-configured scope." + }, + { + "description": "Denies the unregister_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-unregister-all", + "markdownDescription": "Denies the unregister_all command without any pre-configured scope." + }, { "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\n#### This default permission set includes:\n\n- `allow-open`", "type": "string", diff --git a/desktop/src-tauri/gen/schemas/macOS-schema.json b/desktop/src-tauri/gen/schemas/macOS-schema.json index f827fe175..2cb2c2a88 100644 --- a/desktop/src-tauri/gen/schemas/macOS-schema.json +++ b/desktop/src-tauri/gen/schemas/macOS-schema.json @@ -2354,6 +2354,72 @@ "const": "core:window:deny-unminimize", "markdownDescription": "Denies the unminimize command without any pre-configured scope." }, + { + "description": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n", + "type": "string", + "const": "global-shortcut:default", + "markdownDescription": "No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n" + }, + { + "description": "Enables the is_registered command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-is-registered", + "markdownDescription": "Enables the is_registered command without any pre-configured scope." + }, + { + "description": "Enables the register command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-register", + "markdownDescription": "Enables the register command without any pre-configured scope." + }, + { + "description": "Enables the register_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-register-all", + "markdownDescription": "Enables the register_all command without any pre-configured scope." + }, + { + "description": "Enables the unregister command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-unregister", + "markdownDescription": "Enables the unregister command without any pre-configured scope." + }, + { + "description": "Enables the unregister_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:allow-unregister-all", + "markdownDescription": "Enables the unregister_all command without any pre-configured scope." + }, + { + "description": "Denies the is_registered command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-is-registered", + "markdownDescription": "Denies the is_registered command without any pre-configured scope." + }, + { + "description": "Denies the register command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-register", + "markdownDescription": "Denies the register command without any pre-configured scope." + }, + { + "description": "Denies the register_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-register-all", + "markdownDescription": "Denies the register_all command without any pre-configured scope." + }, + { + "description": "Denies the unregister command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-unregister", + "markdownDescription": "Denies the unregister command without any pre-configured scope." + }, + { + "description": "Denies the unregister_all command without any pre-configured scope.", + "type": "string", + "const": "global-shortcut:deny-unregister-all", + "markdownDescription": "Denies the unregister_all command without any pre-configured scope." + }, { "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\n#### This default permission set includes:\n\n- `allow-open`", "type": "string", diff --git a/docs/design-docs/api-client-package-followup.md b/docs/design-docs/api-client-package-followup.md new file mode 100644 index 000000000..7cc141be8 --- /dev/null +++ b/docs/design-docs/api-client-package-followup.md @@ -0,0 +1,30 @@ +# API Client Package Follow-up + +The first extraction of `@spacebot/api-client` is intentionally light. + +It creates a reusable package boundary for Spacedrive and other consumers, but it still re-exports generated types from `interface/src/api/`. + +## Follow-up Work + +### 1. Make `spacebot/interface` consume `@spacebot/api-client` + +The Spacebot interface should stop importing from its local `src/api/*` modules directly and instead consume the shared package. + +That will make the package the single source of truth for: + +- OpenAPI client setup +- generated schema types +- friendly exported types +- manual SSE event types + +### 2. Move OpenAPI generation output into the package + +The long-term target is for `schema.d.ts` to be generated directly into `packages/api-client/` instead of `interface/src/api/`. + +That will let the shared package fully own the generated contract and avoid the current re-export bridge. + +## Why Deferred + +These steps are worth doing, but they are not required for the first Spacedrive integration slice. + +For now, the package boundary exists, and Spacedrive can start consuming it immediately. diff --git a/docs/design-docs/conversation-settings.md b/docs/design-docs/conversation-settings.md new file mode 100644 index 000000000..4eba5f71e --- /dev/null +++ b/docs/design-docs/conversation-settings.md @@ -0,0 +1,1056 @@ +# Conversation & Channel Settings — Unifying Configuration and Sunsetting Cortex Chat + +**Status:** Draft +**Date:** 2026-03-25 +**Context:** The Spacedrive Spacebot interface is gaining multi-conversation support. The existing split between channels (no tools, has personality) and cortex chat (all tools, no personality) creates UX confusion. This doc proposes a unified per-conversation settings model that absorbs cortex chat's capabilities into normal conversations. It also defines worker context settings and renames "webchat" to "portal" throughout the codebase. + +--- + +## Problem + +There are four problems converging: + +### 1. Cortex Chat is a UX Dead End + +Cortex chat exists because channels deliberately lack tools — they delegate to branches and workers. When a user wants to configure something, query memories, or run shell commands directly, they go to cortex chat. But from a user's perspective, "cortex" is an unexplained internal concept. Why is there a separate chat? Why does it behave differently? Why can't I just tell my agent to do this in our normal conversation? + +Cortex chat is one session per agent. It has no personality. It gets a different system prompt. It persists to a separate table (`cortex_chat_messages`). None of this is visible or meaningful to the user — it's implementation leaking into product. + +### 2. No Per-Conversation Configuration + +The portal conversation feature just landed. Users can now create multiple conversations. But every conversation behaves identically — same model, same tools, same memory injection. There is no way to say "this conversation uses Opus" or "this conversation doesn't need memory context" or "let me use tools directly here." + +The model selector in the Spacedrive UI is decorative — Spacebot ignores it because models are configured per process type in TOML, not per conversation. + +### 3. Channel Configuration is Agent-Global + +Channels (Discord, Slack, etc.) have `require_mention` on bindings and `listen_only_mode` in config. But the model, memory injection, and tool access are all agent-level. You can't say "this Discord channel uses Haiku" or "this Slack channel doesn't need working memory." All channels for an agent share the same routing config. + +### 4. Workers Are Context-Starved + +Workers receive only a task description and a static system prompt. No conversation history, no knowledge synthesis, no working memory. The channel must pack everything the worker needs into the task string. This is limiting — a worker can't understand the broader conversation context, can't leverage memory the agent has built up, and can't see what led to the task being created. + +Branches, by contrast, get the full channel history clone plus memory tools. The gap between branch context richness and worker context poverty is entirely hardcoded. There's no way to say "this worker should get conversation history" or "this worker should get the agent's memory context." + +--- + +## What Exists Today + +### Configuration Hierarchy (Current) + +``` +Agent Config (TOML) +├── routing: { channel, branch, worker, compactor, cortex } +│ ├── task_overrides: { "coding" → model } +│ └── fallbacks: { model → [fallback_models] } +├── memory_persistence: { enabled, message_interval } +├── working_memory: { enabled, context_token_budget, ... } +├── cortex: { knowledge_synthesis (change-driven), maintenance, ... } +├── channel_config: { listen_only_mode, save_attachments } +└── bindings[]: { channel, require_mention, ... } +``` + +### Tool Access (Current) + +| Context | Memory Tools | Execution Tools | Delegation Tools | Platform Tools | +|---------|-------------|-----------------|------------------|----------------| +| Channel | No | No | Yes (branch, spawn_worker, route) | Yes (reply, react, skip) | +| Branch | Yes (recall, save, delete) | No | Yes (spawn_worker) | No | +| Worker | No | Yes (shell, file_*, browser) | No | No | +| Cortex Chat | Yes | Yes | Yes (spawn_worker) | No | + +Cortex chat is the only context that combines memory tools + execution tools + worker spawning. That's why it exists — it's the "power mode." + +### Memory Injection (Current) + +The memory system has three layers that get injected into prompts: + +1. **Knowledge Synthesis** (replaced the old "bulletin") — LLM-curated briefing synthesized from the memory graph (decisions, preferences, goals, high-importance facts, active tasks). Change-driven regeneration via dirty-flag + debounce. Primary memory context. +2. **Working Memory** — Temporal event log. Today's events in detail, yesterday compressed to summary, earlier this week as a paragraph. Real-time, rendered on every prompt. +3. **Channel Activity Map** — Cross-channel visibility. What's happening in other channels. Real-time. + +The old bulletin is kept only as a startup fallback if knowledge synthesis fails. The prompt template prefers `knowledge_synthesis`, falls back to `memory_bulletin`, and renders `working_memory` separately. + +| Context | Knowledge Synthesis | Working Memory (Temporal) | Channel Activity Map | Tool-Based Recall | +|---------|-------------------|--------------------------|---------------------|-------------------| +| Channel | Yes | Yes (if enabled) | Yes (if enabled) | No (delegates to branch) | +| Branch | Yes | No | No | Yes | +| Worker | No | No | No | No | +| Cortex Chat | Yes | No | No | Yes | + +### Worker Context (Current) + +When a worker is spawned today: + +1. **System prompt** — Rendered from `worker.md.j2` with filesystem paths, sandbox config, tool secrets, available skills. No memory, no conversation context. +2. **Task description** — The only user-facing input. Becomes the first prompt. Channel must front-load all necessary context into this string. +3. **Skills** — Available skills listed with suggested skills flagged by the channel. +4. **Tools** — shell, file_read/write/edit/list, set_status, read_skill, task_update, optionally browser/web_search/MCP. +5. **No history** — Fire-and-forget workers start with empty history. Interactive workers only get history from prior turns in their own session. +6. **No memory** — No knowledge synthesis, no working memory, no memory tools. + +Compare to branches, which get a **full clone of the channel history** plus memory tools (recall, save, delete) and knowledge synthesis via system prompt. + +--- + +## Proposed Design + +### Core Idea: Conversation Modes + +Every conversation (portal or platform channel) gets a **mode** that determines its behavior. The mode is a small set of flags. Agent config provides defaults. Per-channel and per-conversation overrides are optional. + +### The Settings + +``` +ConversationSettings { + model: Option, // LLM override for this conversation's channel process + memory: MemoryMode, // How memory is used + delegation: DelegationMode, // How tools work + worker_context: WorkerContextMode, // What context workers receive +} +``` + +#### `model: Option` + +When set, overrides `routing.channel` for the channel process in this conversation. Branches and workers spawned from this conversation also inherit the override (overriding `routing.branch` and `routing.worker` respectively). + +When `None`, falls through to agent-level routing config as today. + +This replaces the non-functional model selector in the UI with something that actually works. + +**Implementation:** `routing.resolve()` gains an optional `override_model` parameter. When present, it returns the override instead of the process-type default. Task-type overrides (`task_overrides`) still take priority over per-conversation overrides for workers/branches, because task-type routing is a quality-of-output concern, not a user preference. + +#### `memory: MemoryMode` + +```rust +enum MemoryMode { + Full, // Knowledge synthesis + working memory + channel activity map + auto-persistence (current default) + Ambient, // All memory context injected, but no auto-persistence and no memory tools + Off, // No memory context injected, no memory tools, no persistence +} +``` + +- **Full** — The agent knows everything it normally knows. All three memory layers injected (knowledge synthesis, working memory, channel activity map). Memory persistence branches fire. This is today's default. +- **Ambient** — The agent gets all memory context (knowledge synthesis, working memory, channel activity map) but doesn't write new memories from this conversation. No auto-persistence branches. Useful for throwaway chats or sensitive topics. +- **Off** — Raw mode. No memory context injected — no knowledge synthesis, no working memory, no channel activity map. The conversation is stateless relative to the agent's memory. System prompt still includes identity/personality. This is the spirit of what cortex chat provides today. + +#### `delegation: DelegationMode` + +```rust +enum DelegationMode { + Standard, // Current channel behavior: must delegate via branch/worker + Direct, // Channel gets all tools: memory, shell, file, browser, + delegation tools +} +``` + +- **Standard** — The channel has `reply`, `branch`, `spawn_worker`, `route`, `cancel`, `skip`, `react`. It delegates heavy work. It stays responsive. This is today's default and the right mode for ongoing async conversations. +- **Direct** — The channel gets the full tool set: memory tools, shell, file operations, browser, web search, **plus** delegation tools. The agent can choose to do things directly or spawn workers. This is what cortex chat provides today — the "power user" mode. + +**Why not just "tools on/off"?** Because turning tools "off" for a channel is meaningless — it already barely has tools. The meaningful toggle is whether the channel can act directly or must delegate. "Direct" mode is strictly additive. + +#### `worker_context: WorkerContextMode` + +```rust +struct WorkerContextMode { + history: WorkerHistoryMode, // What conversation context the worker sees + memory: WorkerMemoryMode, // What memory context the worker gets +} + +enum WorkerHistoryMode { + None, // No conversation history (current default) + Summary, // LLM-generated summary of recent conversation context + Recent(u32), // Last N messages from the parent conversation, injected into system prompt + Full, // Full conversation history clone (branch-style) +} + +enum WorkerMemoryMode { + None, // No memory context (current default) + Ambient, // Knowledge synthesis + working memory injected into system prompt (read-only) + Tools, // Ambient context + memory_recall tool (can search but not write) + Full, // Ambient context + full memory tools (recall, save, delete) — branch-level access +} +``` + +This is the most important new axis. Today, the gap between "worker" and "branch" is a cliff — branches see everything, workers see nothing. Worker context settings turn this into a spectrum. + +**`WorkerHistoryMode` options:** + +- **None** — Current behavior. Worker sees only the task description. Channel must front-load all context into the task string. Cheapest, most isolated, fastest to start. +- **Summary** — Before spawning the worker, the channel generates a brief summary of the relevant conversation context and prepends it to the worker's system prompt. Cost: one extra LLM call at spawn time (can use a cheap model). Benefit: worker understands why the task exists without seeing the full transcript. +- **Recent(N)** — Last N conversation messages injected into the worker's system prompt as context (similar to how cortex chat injects channel context). No extra LLM call. Worker sees recent exchanges but not the full history. Good middle ground. +- **Full** — Worker receives a clone of the full channel history, same as a branch. Most expensive in tokens, but gives the worker complete conversational context. + +**`WorkerMemoryMode` options:** + +- **None** — Current behavior. No memory context, no memory tools. Worker is a pure executor. +- **Ambient** — Knowledge synthesis + working memory injected into the worker's system prompt. Worker has ambient awareness of what the agent knows — identity, facts, preferences, decisions, recent activity — but can't search or write. Read-only, no extra tools, minimal cost. +- **Tools** — Ambient context + `memory_recall` tool. Worker can actively search the agent's memory when it needs context. Cannot save new memories (that's still a branch concern). This is the "informed executor" mode. +- **Full** — Ambient context + full memory tools (recall, save, delete). Worker operates at branch-level memory access. Use sparingly — this blurs the worker/branch boundary. + +**Why this matters for the Spacedrive interface:** When a user starts a "Hands-on" conversation in Spacedrive (Direct delegation, memory off), the workers spawned from that conversation should probably get more context than workers spawned from a Discord channel with 50 participants. The user is sitting right there, interacting directly — the workers are extensions of that focused session. + +**Implementation:** Worker context settings are resolved at spawn time in `spawn_worker_from_state()`: + +```rust +fn spawn_worker_with_context( + state: &ChannelState, + task: &str, + settings: &ResolvedConversationSettings, +) { + let mut system_prompt = render_worker_prompt(...); + + // Inject memory context + match settings.worker_context.memory { + WorkerMemoryMode::None => { /* current behavior */ } + WorkerMemoryMode::Ambient | WorkerMemoryMode::Tools | WorkerMemoryMode::Full => { + // Inject knowledge synthesis (primary memory context) + let knowledge = state.deps.runtime_config.knowledge_synthesis(); + system_prompt.push_str(&format!("\n\n## Knowledge Context\n{knowledge}")); + // Inject working memory (temporal context) + if let Some(wm) = render_working_memory(&state.deps.working_memory_store, &config).await { + system_prompt.push_str(&format!("\n\n{wm}")); + } + } + } + + // Build history + let history = match settings.worker_context.history { + WorkerHistoryMode::None => vec![], + WorkerHistoryMode::Summary => { + let summary = generate_context_summary(state, task).await; + // Inject as a system message at the start + vec![Message::system(format!("[Conversation context]: {summary}"))] + } + WorkerHistoryMode::Recent(n) => { + let h = state.history.read().await; + h.iter().rev().take(n as usize).rev().cloned().collect() + } + WorkerHistoryMode::Full => { + let h = state.history.read().await; + h.clone() + } + }; + + // Build tool server + let mut tool_server = create_worker_tool_server(...); + match settings.worker_context.memory { + WorkerMemoryMode::Tools => { + tool_server.add(MemoryRecallTool::new(...)); + } + WorkerMemoryMode::Full => { + tool_server.add(MemoryRecallTool::new(...)); + tool_server.add(MemorySaveTool::new(...)); + tool_server.add(MemoryDeleteTool::new(...)); + } + _ => {} + } + + Worker::new(..., task, system_prompt, history, tool_server) +} +``` + +### Configuration Hierarchy (Proposed) + +``` +Agent Config (TOML) — defaults for all conversations +├── defaults.conversation_settings: +│ ├── model: None (use routing config) +│ ├── memory: Full +│ ├── delegation: Standard +│ └── worker_context: { history: None, memory: None } +│ +├── Per-Channel Override (TOML or runtime API) +│ └── channels["discord:guild:channel"].settings: +│ ├── model: "anthropic/claude-haiku-4.5" +│ ├── memory: Ambient +│ ├── delegation: Standard +│ └── worker_context: { history: None, memory: Ambient } +│ +└── Per-Conversation Override (runtime, stored in DB) + └── portal_conversations.settings: + ├── model: "anthropic/claude-opus-4" + ├── memory: Off + ├── delegation: Direct + └── worker_context: { history: Recent(20), memory: Tools } +``` + +**Resolution order:** Per-conversation > Per-channel > Agent default > System default + +For portal conversations, "per-channel" doesn't apply (there's no binding). For platform channels, "per-conversation" doesn't apply (Discord threads aren't separate conversations in this model — they share the channel's settings). + +### Sunsetting Cortex Chat + +With these settings, cortex chat becomes a conversation preset, not a separate system: + +**"Cortex mode" conversation** = `{ model: None, memory: Off, delegation: Direct, worker_context: { history: Recent(20), memory: Tools } }` + +The user starts a new conversation, toggles delegation to Direct, turns memory off, and they have cortex chat. No separate concept, no separate table, no separate API. + +**Migration path:** + +1. Add `ConversationSettings` to `portal_conversations` table (JSON column). +2. Add `ConversationSettings` to channel config (TOML + runtime). +3. Wire settings into channel creation — read settings when spawning a channel process. +4. In `Direct` mode, use `create_cortex_chat_tool_server()` (or a new unified factory) instead of `add_channel_tools()`. +5. In `Off` memory mode, skip knowledge synthesis, working memory, and channel activity map injection in the system prompt. +6. Deprecate `/api/cortex-chat/*` endpoints. Keep them working but add a sunset header. +7. Remove cortex chat from the UI. Replace with conversation presets. + +**What about cortex chat's channel context injection?** Today, opening cortex chat on a channel page injects the last 50 messages from that channel. This is useful. In the new model, a "Direct" conversation can have a `channel_context` field — "I'm looking at Discord #general" — and the system prompt injects that context. This is an optional enhancement, not a blocker. + +**What about cortex chat's admin-only access?** Cortex chat is implicitly admin-only because it's in the dashboard. In the portal/Spacedrive context, all users are the owner. If multi-user access control becomes needed, it should be a separate authorization layer, not a property of conversation mode. + +--- + +## Schema Changes + +### `portal_conversations` table (renamed from `webchat_conversations`) + +Add a `settings` JSON column: + +```sql +ALTER TABLE portal_conversations ADD COLUMN settings TEXT; +-- JSON: {"model": "...", "memory": "full|ambient|off", "delegation": "standard|direct", +-- "worker_context": {"history": "none|summary|recent:20|full", "memory": "none|ambient|tools|full"}} +-- NULL means "use defaults" +``` + +### `channels` table + +Add a `settings` JSON column: + +```sql +ALTER TABLE channels ADD COLUMN settings TEXT; +-- Same schema as above. NULL means "use agent defaults" +``` + +### Config TOML + +```toml +[defaults.conversation_settings] +memory = "full" # "full", "ambient", "off" +delegation = "standard" # "standard", "direct" +# model omitted = use routing config + +[defaults.conversation_settings.worker_context] +history = "none" # "none", "summary", "recent:20", "full" +memory = "none" # "none", "ambient", "tools", "full" +``` + +Per-channel overrides in bindings: + +```toml +[[bindings]] +agent_id = "star" +channel = "discord" +guild_id = "123456" +channel_ids = ["789"] + +[bindings.settings] +model = "anthropic/claude-haiku-4.5" +memory = "ambient" +delegation = "standard" + +[bindings.settings.worker_context] +history = "none" +memory = "ambient" +``` + +--- + +## API Changes + +### Portal Endpoints (renamed from `/webchat/*`) + +**POST /portal/conversations** — Add optional `settings` field: + +```json +{ + "agent_id": "star", + "title": "Quick coding help", + "settings": { + "model": "anthropic/claude-opus-4", + "memory": "off", + "delegation": "direct", + "worker_context": { + "history": "recent:20", + "memory": "tools" + } + } +} +``` + +**PUT /portal/conversations/{id}** — Allow updating `settings`: + +```json +{ + "agent_id": "star", + "settings": { + "memory": "ambient" + } +} +``` + +Settings changes take effect on the **next message** in the conversation. In-flight channel processes are not interrupted. + +**GET /portal/conversations** — Return `settings` in response (null = defaults). + +**Full endpoint rename:** + +| Old | New | +|-----|-----| +| `POST /webchat/send` | `POST /portal/send` | +| `GET /webchat/history` | `GET /portal/history` | +| `GET /webchat/conversations` | `GET /portal/conversations` | +| `POST /webchat/conversations` | `POST /portal/conversations` | +| `PUT /webchat/conversations/{id}` | `PUT /portal/conversations/{id}` | +| `DELETE /webchat/conversations/{id}` | `DELETE /portal/conversations/{id}` | + +### New Endpoint: GET /api/conversation-defaults + +Returns the resolved default settings for new conversations: + +```json +{ + "model": "anthropic/claude-sonnet-4", + "memory": "full", + "delegation": "standard", + "worker_context": { + "history": "none", + "memory": "none" + }, + "available_models": ["anthropic/claude-sonnet-4", "anthropic/claude-opus-4", "anthropic/claude-haiku-4.5"], + "memory_modes": ["full", "ambient", "off"], + "delegation_modes": ["standard", "direct"], + "worker_history_modes": ["none", "summary", "recent", "full"], + "worker_memory_modes": ["none", "ambient", "tools", "full"] +} +``` + +This replaces the hardcoded model list in the Spacedrive UI. + +### Channel Settings Endpoint + +**PUT /api/channels/{id}/settings** — Update per-channel settings: + +```json +{ + "agent_id": "star", + "settings": { + "model": "anthropic/claude-haiku-4.5", + "memory": "ambient", + "worker_context": { + "memory": "ambient" + } + } +} +``` + +**GET /api/channels/{id}** — Return `settings` in channel info. + +--- + +## Implementation in the Channel Process + +### Channel Creation Changes + +When a channel is created (`main.rs` routing logic), resolve settings: + +```rust +fn resolve_conversation_settings( + agent_config: &AgentConfig, + channel_settings: Option<&ConversationSettings>, + conversation_settings: Option<&ConversationSettings>, +) -> ResolvedConversationSettings { + let defaults = &agent_config.conversation_settings; + + // Per-conversation > Per-channel > Agent default + ResolvedConversationSettings { + model: conversation_settings.and_then(|s| s.model.clone()) + .or_else(|| channel_settings.and_then(|s| s.model.clone())) + .or_else(|| defaults.model.clone()), + memory: conversation_settings.map(|s| s.memory) + .or_else(|| channel_settings.map(|s| s.memory)) + .unwrap_or(defaults.memory), + delegation: conversation_settings.map(|s| s.delegation) + .or_else(|| channel_settings.map(|s| s.delegation)) + .unwrap_or(defaults.delegation), + worker_context: conversation_settings.and_then(|s| s.worker_context.clone()) + .or_else(|| channel_settings.and_then(|s| s.worker_context.clone())) + .unwrap_or_else(|| defaults.worker_context.clone()), + } +} +``` + +### Tool Server Selection + +In `run_agent_turn()`, check delegation mode: + +```rust +match self.resolved_settings.delegation { + DelegationMode::Standard => { + // Current behavior: add_channel_tools() + self.tool_server.add_channel_tools(...); + } + DelegationMode::Direct => { + // Full tool access: memory + execution + delegation + // Use a new factory or merge channel + cortex tool sets + self.tool_server.add_direct_mode_tools(...); + } +} +``` + +### Memory Injection + +In `build_system_prompt()`, check memory mode: + +```rust +match self.resolved_settings.memory { + MemoryMode::Full => { + // Inject all memory layers + enable persistence branches + prompt.set("knowledge_synthesis", &knowledge_synthesis); + prompt.set("working_memory", &working_memory_context); + prompt.set("channel_activity_map", &channel_activity_map); + } + MemoryMode::Ambient => { + // Inject all memory layers, but no persistence + prompt.set("knowledge_synthesis", &knowledge_synthesis); + prompt.set("working_memory", &working_memory_context); + prompt.set("channel_activity_map", &channel_activity_map); + // Skip scheduling persistence branches + } + MemoryMode::Off => { + // No memory context at all + prompt.set("knowledge_synthesis", ""); + prompt.set("working_memory", ""); + prompt.set("channel_activity_map", ""); + } +} +``` + +### Model Override + +In the LLM call, check for model override: + +```rust +let model_name = match &self.resolved_settings.model { + Some(override_model) => override_model.clone(), + None => routing.resolve(ProcessType::Channel, None).to_string(), +}; +``` + +For spawned workers and branches, propagate the override: + +```rust +// In spawn_worker +let worker_model = match &self.resolved_settings.model { + Some(override_model) => override_model.clone(), + None => routing.resolve(ProcessType::Worker, task_type).to_string(), +}; +``` + +--- + +## Interface Changes — Separation of Concerns + +There are three interface surfaces that interact with this feature. The critical constraint is: **do not break the legacy Spacebot dashboard while building the Spacedrive experience.** + +### The Three Surfaces + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Spacebot Server (Rust) │ +│ ├── /api/portal/* — portal conversation endpoints │ +│ ├── /api/channels/* — channel endpoints │ +│ ├── /api/conversation-defaults — new settings metadata │ +│ └── /api/cortex-chat/* — cortex chat (deprecated later) │ +├─────────────────────────────────────────────────────────────────┤ +│ @spacebot/api-client (shared TS package) │ +│ ├── portalSend(), portalHistory(), etc. │ +│ ├── createPortalConversation({ settings? }) ← new field │ +│ ├── updatePortalConversation({ settings? }) ← new field │ +│ ├── getConversationDefaults() ← new method │ +│ └── All existing methods remain unchanged │ +├──────────────────────┬──────────────────────────────────────────┤ +│ Spacebot Dashboard │ Spacedrive Interface │ +│ (spacebot/interface)│ (spacedrive/.../Spacebot/) │ +│ │ │ +│ ✗ NO changes needed │ ✓ All new settings UI goes here │ +│ - AgentConfig keeps │ - SpacebotContext gets settings state │ +│ its model routing │ - ChatComposer gets settings controls │ +│ - CortexChatPanel │ - ConversationScreen shows active mode │ +│ keeps working │ - New conversation gets settings picker │ +│ - ChannelDetail │ - Presets for common configurations │ +│ unchanged │ │ +│ - Settings page │ │ +│ unchanged │ │ +└──────────────────────┴──────────────────────────────────────────┘ +``` + +### Why the Dashboard Doesn't Change + +The Spacebot dashboard (`spacebot/interface/`) is a developer-facing admin control plane. It already has: + +- **Agent-level model routing** in `AgentConfig.tsx` — 6 model slots (channel, branch, worker, compactor, cortex, voice) with `ModelSelect` component and adaptive thinking controls. These are agent-wide defaults. They keep working. The new per-conversation settings are *overrides* on top of these defaults. +- **Cortex chat** in `CortexChatPanel.tsx` — used in the Cortex route and Channel Detail views. This keeps working until Phase 4 sunset. No dashboard changes needed for deprecation — we just stop rendering it later. +- **Channel management** in `ChannelDetail.tsx` — read-only channel timeline with cortex panel. No per-channel settings UI exists today. Adding per-channel settings to the dashboard is a *future* enhancement, not a blocker. +- **Global settings** in `Settings.tsx` (3000+ lines) — provider credentials, channel bindings, server config. None of this changes. + +The dashboard consumes the same `@spacebot/api-client` package, but it never calls `createPortalConversation()` with settings because its `WebChatPanel` component creates conversations without settings (defaulting to `null`). The new `settings` field is optional — `null` means "use agent defaults." Existing code that doesn't pass settings continues working identically. + +### What Changes in @spacebot/api-client + +All changes are **additive**. No breaking changes to existing methods. + +**Renamed methods (Phase 0 — portal rename):** + +```typescript +// Old (removed after rename) // New +apiClient.webchatSend(...) → apiClient.portalSend(...) +apiClient.webchatHistory(...) → apiClient.portalHistory(...) +apiClient.listWebchatConversations() → apiClient.listPortalConversations() +apiClient.createWebchatConversation()→ apiClient.createPortalConversation() +apiClient.updateWebchatConversation()→ apiClient.updatePortalConversation() +apiClient.deleteWebchatConversation()→ apiClient.deletePortalConversation() +``` + +The rename touches both surfaces simultaneously — the Spacebot dashboard's `WebChatPanel` and `AgentChat.tsx` import from api-client too. This is a coordinated rename across both consumers. It's mechanical but must be done in one pass. + +**New methods (Phase 1):** + +```typescript +// Fetch resolved defaults + available options for settings UI +apiClient.getConversationDefaults(agentId: string): Promise + +// Types +interface ConversationDefaultsResponse { + model: string; // Current default model name + memory: MemoryMode; // Current default memory mode + delegation: DelegationMode; // Current default delegation mode + worker_context: WorkerContextMode; // Current default worker context + available_models: ModelOption[]; // All available models with metadata + memory_modes: MemoryMode[]; + delegation_modes: DelegationMode[]; + worker_history_modes: string[]; + worker_memory_modes: string[]; +} + +interface ModelOption { + id: string; // e.g. "anthropic/claude-sonnet-4" + name: string; // e.g. "Claude Sonnet 4" + provider: string; // e.g. "anthropic" + context_window: number; + supports_tools: boolean; + supports_thinking: boolean; +} + +type MemoryMode = 'full' | 'ambient' | 'off'; +type DelegationMode = 'standard' | 'direct'; + +interface WorkerContextMode { + history: string; // "none" | "summary" | "recent:N" | "full" + memory: string; // "none" | "ambient" | "tools" | "full" +} + +interface ConversationSettings { + model?: string | null; + memory?: MemoryMode; + delegation?: DelegationMode; + worker_context?: WorkerContextMode; +} +``` + +**Updated method signatures (additive):** + +```typescript +// createPortalConversation gains optional settings +apiClient.createPortalConversation(input: { + agentId: string; + title?: string | null; + settings?: ConversationSettings | null; // ← new, optional +}): Promise + +// updatePortalConversation gains optional settings +apiClient.updatePortalConversation(input: { + agentId: string; + sessionId: string; + title?: string | null; + archived?: boolean; + settings?: ConversationSettings | null; // ← new, optional +}): Promise + +// PortalConversationResponse and PortalConversationSummary gain settings field +interface PortalConversationResponse { + // ...existing fields... + settings: ConversationSettings | null; // ← new, nullable +} + +interface PortalConversationSummary { + // ...existing fields... + settings: ConversationSettings | null; // ← new, nullable +} +``` + +**Dashboard impact:** The dashboard's `WebChatPanel` calls `createWebchatConversation({ agentId, title })` (soon `createPortalConversation`). After the type update, `settings` is optional, so this call continues to work — settings defaults to `null`, which means "use agent defaults." The dashboard never needs to pass settings unless it wants to. The `settings` field appears in the response type but the dashboard's list rendering doesn't use it — it only shows title, preview, and timestamp. + +### What Changes in Spacedrive Interface + +All new settings UI lives in `spacedrive/packages/interface/src/Spacebot/`. Here's the file-by-file breakdown. + +#### `SpacebotContext.tsx` — State & Data + +**Remove hardcoded data:** +```typescript +// DELETE these static arrays +export const modelOptions = ['Claude 3.7 Sonnet', 'GPT-5', 'Qwen 2.5 72B']; +export const projectOptions = ['Spacedrive v3', 'Spacebot Runtime', 'Hosted Platform']; +``` + +**Add settings state:** +```typescript +// New state in SpacebotContext +conversationDefaults: ConversationDefaultsResponse | null; // from API +conversationSettings: ConversationSettings; // current selection +setConversationSettings: (s: Partial) => void; +``` + +**Add defaults query:** +```typescript +// New TanStack Query — fetches available models, modes, defaults +const defaultsQuery = useQuery({ + queryKey: ['spacebot', 'conversation-defaults', selectedAgent], + queryFn: () => apiClient.getConversationDefaults(selectedAgent), + staleTime: 60_000, // Defaults don't change often +}); +``` + +**Update conversation creation:** +```typescript +// handleSendMessage — when creating a new conversation, pass settings +const conversation = await apiClient.createPortalConversation({ + agentId: selectedAgent, + title: null, + settings: hasNonDefaultSettings(conversationSettings) + ? conversationSettings + : null, // Don't store if all defaults +}); +``` + +**Add settings from conversation response:** +```typescript +// When loading a conversation, read its settings +const activeConversationSettings: ConversationSettings | null = + activeConversation?.settings ?? null; + +// Resolved settings: conversation-specific if set, else defaults +const resolvedSettings: ConversationSettings = { + model: activeConversationSettings?.model ?? conversationDefaults?.model ?? null, + memory: activeConversationSettings?.memory ?? conversationDefaults?.memory ?? 'full', + delegation: activeConversationSettings?.delegation ?? conversationDefaults?.delegation ?? 'standard', + worker_context: activeConversationSettings?.worker_context ?? conversationDefaults?.worker_context ?? { history: 'none', memory: 'none' }, +}; +``` + +**Expose through context:** +```typescript +// Added to SpacebotContextType +conversationDefaults: ConversationDefaultsResponse | null; +resolvedSettings: ConversationSettings; +updateConversationSettings: (patch: Partial) => Promise; +``` + +The `updateConversationSettings` mutation calls `apiClient.updatePortalConversation()` with the new settings and invalidates the conversation query. + +#### `ChatComposer.tsx` — Settings Controls + +The composer currently has project and model selector popovers that are non-functional. Replace them with real settings controls. + +**Replace model selector:** +``` +Before: Static dropdown cycling through ['Claude 3.7 Sonnet', 'GPT-5', 'Qwen 2.5 72B'] +After: Real model dropdown populated from conversationDefaults.available_models + Selecting a model calls updateConversationSettings({ model: selectedId }) +``` + +**Replace project selector with memory/delegation toggles:** +``` +Before: Static dropdown cycling through project names (unused) +After: Two compact selectors: + Memory: [On ▾] / [Context Only ▾] / [Off ▾] + Mode: [Standard ▾] / [Direct ▾] +``` + +The composer bottom bar becomes: + +``` +┌──────────────────────────────────────────────────────┐ +│ [textarea input area] │ +│ │ +│ [Model: Sonnet 4 ▾] [Memory: On ▾] [Mode ▾] [⎈] │ +└──────────────────────────────────────────────────────┘ + ^ + settings gear → opens + full settings panel + with worker context +``` + +The `[⎈]` gear icon opens a popover/panel for advanced settings (worker context, presets). The three inline selectors cover the most common toggles without requiring the panel. + +**New component: `ConversationSettingsPanel.tsx`** + +``` +spacedrive/packages/interface/src/Spacebot/ConversationSettingsPanel.tsx +``` + +A panel (popover or slide-out) with: + +``` +Model +[Sonnet 4 ▾] — searchable dropdown from available_models + +Memory +( ) On — Full memory context, auto-persistence +( ) Context Only — Memory context visible, no writes +( ) Off — No memory context + +Mode +( ) Standard — Delegates to workers and branches +( ) Direct — Full tool access (shell, files, memory) + +Worker Context (collapsed by default) + History: [None ▾] — None / Summary / Recent (20) / Full + Memory: [None ▾] — None / Ambient / Tools / Full + +Presets +[Chat] [Focus] [Hands-on] [Quick] [Deep Work] +``` + +Presets are pill buttons that set all fields at once. Selecting one updates the form. User can then tweak individual fields. + +#### `ConversationScreen.tsx` — Active Settings Display + +Show the active settings inline below the conversation header: + +``` +Conversation Title +Sonnet 4 · Memory On · Standard [⎈] +───────────────────────────────────────────────────── +[messages...] +``` + +This is a single line of muted text showing the resolved settings. Clicking `[⎈]` opens the settings panel for editing. If all settings are defaults, this line can be hidden or show just the model name. + +When settings are `Direct` delegation, show a visual indicator (e.g., a subtle border color change or badge) so the user knows the conversation has tool access. + +#### `EmptyChatHero.tsx` — Preset Selection + +The empty chat hero currently says "Let's get to work, James." Update it to: + +1. Replace "James" with the actual user name (from library context or platform). +2. Show preset cards below the greeting: + +``` +Let's get to work, Jamie + +How would you like to work? + +[💬 Chat] [🔧 Hands-on] [⚡ Quick] +Normal Direct tools Fast & cheap +conversation replaces cortex throwaway +``` + +Selecting a preset sets `conversationSettings` in context and focuses the composer. The user's first message creates the conversation with those settings. + +#### `SpacebotLayout.tsx` — No Structural Changes + +The layout (sidebar + topbar + content area) doesn't change structurally. The sidebar conversation list could optionally show a small icon or badge indicating non-default settings (e.g., a wrench icon for Direct mode conversations), but this is polish, not required. + +#### New File: `ConversationSettingsPanel.tsx` + +``` +spacedrive/packages/interface/src/Spacebot/ConversationSettingsPanel.tsx +``` + +This is the only new file. It's a self-contained panel component that: +- Reads `conversationDefaults` and `resolvedSettings` from context +- Renders model dropdown, memory radio group, delegation radio group, worker context dropdowns +- Renders preset buttons +- Calls `updateConversationSettings()` on change +- Uses @sd/ui primitives (DropdownMenu, RadioGroup, Button) and semantic colors + +#### Routes — No New Routes + +No new routes needed. Settings are accessed from within existing conversation views via the settings panel, not from a separate page. The stub routes (Tasks, Memories, Autonomy, Schedule) are unrelated and don't change. + +### Conversation Presets + +Common configurations get named presets: + +| Preset | Model | Memory | Delegation | Worker History | Worker Memory | Use Case | +|--------|-------|--------|------------|----------------|---------------|----------| +| Chat | Default | On | Standard | None | None | Normal conversation | +| Focus | Default | Context Only | Standard | None | None | Sensitive topic, no memory writes | +| Hands-on | Default | Off | Direct | Recent(20) | Tools | Direct tool use, replaces cortex | +| Quick | Haiku | Off | Standard | None | None | Fast, cheap, throwaway | +| Deep Work | Opus | On | Standard | Summary | Ambient | Long-running complex tasks with rich worker context | + +Presets are UI sugar — they set the fields. Users can customize after selecting a preset. Presets are defined client-side in the Spacedrive interface, not stored on the server. + +### Channel Settings (Spacebot Dashboard — Future) + +For platform channels (Discord, Slack), per-channel settings can eventually be added to the Spacebot dashboard's `ChannelDetail.tsx`. This is **not part of the initial implementation** — it requires changes to the dashboard, which we're avoiding. The backend API (`PUT /api/channels/{id}/settings`) supports it, but the dashboard UI can be added later when the feature is proven in the Spacedrive interface. + +``` +Future: #general (Discord) +├── Model: [Sonnet 4 ▾] (override / use default) +├── Memory: [On ▾] +├── Mode: [Standard ▾] +├── Mention Only: [Yes/No] +└── [Advanced: Worker Settings] + ├── History: [None ▾] + └── Memory: [None ▾] +``` + +--- + +## Webchat → Portal Rename + +### Motivation + +"Webchat" and "portal" have been two names for the same thing since the beginning. The conversation ID format already uses `portal:chat:{agent_id}:{uuid}`. The adapter registers as `"webchat"` but the platform is extracted as `"portal"`. This inconsistency has been called out in `multi-agent-communication-graph.md` as needing resolution. + +The name **"portal"** wins because: +- It's already the canonical ID prefix +- It's product-facing (webchat is implementation) +- It describes what it is — a portal into the agent, usable from any surface (browser, Spacedrive, mobile) +- "Webchat" implies a web-only chat widget, which undersells what this is + +### Scope + +This rename touches: + +**Rust source (30 files):** +- 3 module files: `src/api/webchat.rs` → `src/api/portal.rs`, `src/messaging/webchat.rs` → `src/messaging/portal.rs`, `src/conversation/webchat.rs` → `src/conversation/portal.rs` +- 18 struct/type names: `WebChat*` → `Portal*` (e.g., `WebChatConversation` → `PortalConversation`, `WebChatAdapter` → `PortalAdapter`) +- 24 function names: `webchat_*` → `portal_*` +- 8 string literals: `"webchat"` → `"portal"` (adapter name, message source) +- Module declarations in `src/api.rs`, `src/conversation.rs`, `src/messaging.rs` +- References in `src/main.rs`, `src/api/state.rs`, `src/api/server.rs`, `src/tools.rs`, `src/config/types.rs` + +**SQL migration:** +- New migration: `ALTER TABLE webchat_conversations RENAME TO portal_conversations` +- Rename index: `idx_webchat_conversations_agent_updated` → `idx_portal_conversations_agent_updated` + +**TypeScript/React:** +- `interface/src/components/WebChatPanel.tsx` → `PortalChatPanel.tsx` +- `interface/src/hooks/useWebChat.ts` → `usePortal.ts` +- `packages/api-client/src/client.ts`: all `webchat*` methods → `portal*` +- Generated types in `interface/src/api/schema.d.ts` and `types.ts` +- Import references in `interface/src/routes/AgentChat.tsx` + +**API endpoints:** +- `/webchat/send` → `/portal/send` +- `/webchat/history` → `/portal/history` +- `/webchat/conversations` → `/portal/conversations` (CRUD) + +**OpenAPI tags:** `"webchat"` → `"portal"` + +**Documentation:** References in 7 design docs and README. + +### Migration Strategy + +1. **Rename Rust modules and types** — Mechanical rename. `WebChat` → `Portal` everywhere. +2. **Rename SQL table** — Single migration: `ALTER TABLE webchat_conversations RENAME TO portal_conversations`. SQLite supports this natively. +3. **Rename API routes** — Update route registration in `server.rs`. Add temporary redirects from old paths for any external consumers. +4. **Rename adapter** — `PortalAdapter::name()` returns `"portal"`. Update source literals. Remove the `"portal" => "webchat:{id}"` remapping hack in `tools.rs` since the names now match. +5. **Rename TypeScript** — Rename files, update imports, regenerate OpenAPI types. +6. **Conversation ID prefix stays `portal:chat:`** — Already correct. No data migration needed. +7. **Update design docs** — Search-and-replace in markdown files. + +This rename is safe to do in a single commit. The "webchat" name is internal — no external API consumers depend on it (the Spacedrive interface uses the `@spacebot/api-client` package which abstracts the endpoints). + +--- + +## What This Replaces + +| Today | After | +|-------|-------| +| Cortex chat (separate system) | "Direct" mode conversation | +| Cortex chat API (`/api/cortex-chat/*`) | Deprecated, then removed | +| `cortex_chat_messages` table | No longer written to | +| `CortexChatSession` struct | Removed | +| Hardcoded model selector (non-functional) | Per-conversation model override (functional) | +| Agent-global memory settings | Per-conversation memory mode | +| Agent-global tool access | Per-conversation delegation mode | +| Context-starved workers | Configurable worker context (history + memory) | +| "webchat" naming confusion | Unified "portal" naming | +| `webchat_conversations` table | `portal_conversations` table | + +--- + +## Migration + +### Phase 0 — Portal Rename + +Rename webchat → portal throughout the codebase. This is a prerequisite because it's purely mechanical and removes naming confusion before the settings work begins. + +1. Rename Rust modules, structs, functions, string literals. +2. Add SQL migration to rename table and index. +3. Rename TypeScript files, components, hooks, API methods. +4. Update API route paths. +5. Remove `portal → webchat` remapping hack in tools.rs. +6. Update documentation. + +### Phase 1 — Add Settings Infrastructure + +1. Add `settings` column to `portal_conversations` and `channels` tables. +2. Add `ConversationSettings` struct to config types (with `WorkerContextMode`). +3. Add `conversation_settings` defaults to agent config. +4. Add `resolve_conversation_settings()` function. +5. Wire settings into conversation create/update API. +6. Add `/api/conversation-defaults` endpoint. + +### Phase 2 — Wire Into Channel Process + +1. Pass resolved settings into `Channel::new()`. +2. Branch tool server selection on `delegation` mode. +3. Branch memory injection on `memory` mode. +4. Branch model resolution on `model` override. +5. Wire worker context settings into `spawn_worker_from_state()`. +6. Add knowledge synthesis + working memory injection and history cloning to worker creation based on settings. +7. Add memory tools to worker tool server when `worker_context.memory` is `Tools` or `Full`. + +### Phase 3 — Wire Into UI + +1. Add settings controls to new conversation dialog in Spacedrive. +2. Add settings display/edit to conversation header. +3. Add presets. +4. Replace non-functional model selector. +5. Add advanced worker context settings (collapsed by default). + +### Phase 4 — Sunset Cortex Chat + +1. Stop showing cortex chat in dashboard UI. +2. Add deprecation notice to cortex chat API endpoints. +3. Offer migration: convert existing cortex chat threads to portal conversations with `Direct` mode. +4. Eventually remove `CortexChatSession`, cortex chat API routes, and `cortex_chat_messages` table. + +--- + +## Open Questions + +1. **Should model override propagate to workers unconditionally?** If a user picks Opus for a conversation and the agent spawns 5 workers, that's expensive. Option: override only applies to the channel process, workers still use routing defaults. Or: show estimated cost in the UI. + +2. **Should "Direct" mode keep personality?** Cortex chat strips personality. But a "Direct" conversation might feel better with personality intact — you're still talking to your agent, just with more power. Proposal: keep personality, add a "technical mode" flag separately if needed. + +3. **Per-channel settings for Discord threads?** Discord threads inherit their parent channel's settings. Should a user be able to override per-thread? Probably not in v1 — threads are ephemeral. + +4. **Settings changes mid-conversation?** Proposed: take effect on next message. But what if the user switches from Standard to Direct mid-conversation — does the history context change? The channel process would need to reload its tool server. This is doable (tools are per-turn already) but needs care. + +5. **Should `Ambient` mode still allow explicit memory tool use in Direct mode?** If delegation is Direct and memory is Ambient, the agent has memory tools but we said "no writes." Option: Ambient removes `memory_save` tool but keeps `memory_recall`. Off removes both. + +6. **Channel-level settings via TOML vs API?** TOML is static and requires restart. API is runtime. Proposal: support both. TOML provides initial defaults, API allows runtime changes that persist to DB. DB overrides TOML. + +7. **Worker context cost guardrails?** `WorkerHistoryMode::Full` with a long conversation could inject thousands of tokens into every worker. Should there be a token budget cap? A max message count even in Full mode? Or is that the user's problem — they opted in. + +8. **Summary mode implementation?** `WorkerHistoryMode::Summary` requires an extra LLM call at spawn time. Which model? The compactor model (cheap, fast)? The conversation's override model? A hardcoded summarizer? This adds latency to worker spawning. diff --git a/interface/src/api/schema.d.ts b/interface/src/api/schema.d.ts index e5f92ef66..f738c27ff 100644 --- a/interface/src/api/schema.d.ts +++ b/interface/src/api/schema.d.ts @@ -591,6 +591,40 @@ export interface paths { patch?: never; trace?: never; }; + "/agents/workers": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List worker runs for an agent, with live status merged from StatusBlocks. */ + get: operations["list_workers"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/agents/workers/detail": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get full detail for a single worker run, including decompressed transcript. */ + get: operations["worker_detail"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/bindings": { parameters: { query?: never; @@ -790,7 +824,7 @@ export interface paths { patch?: never; trace?: never; }; - "/cortex/chat/messages": { + "/cortex-chat/messages": { parameters: { query?: never; header?: never; @@ -811,7 +845,7 @@ export interface paths { patch?: never; trace?: never; }; - "/cortex/chat/send": { + "/cortex-chat/send": { parameters: { query?: never; header?: never; @@ -836,15 +870,14 @@ export interface paths { patch?: never; trace?: never; }; - "/cortex/chat/threads": { + "/cortex-chat/thread": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** List all cortex chat threads for an agent, newest first. */ - get: operations["cortex_chat_threads"]; + get?: never; put?: never; post?: never; /** Delete a cortex chat thread and all its messages. */ @@ -854,6 +887,23 @@ export interface paths { patch?: never; trace?: never; }; + "/cortex-chat/threads": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List all cortex chat threads for an agent, newest first. */ + get: operations["cortex_chat_threads"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/cortex/events": { parameters: { query?: never; @@ -1887,23 +1937,23 @@ export interface paths { patch?: never; trace?: never; }; - "/webchat/history": { + "/webchat/conversations": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - get: operations["webchat_history"]; + get: operations["list_webchat_conversations"]; put?: never; - post?: never; + post: operations["create_webchat_conversation"]; delete?: never; options?: never; head?: never; patch?: never; trace?: never; }; - "/webchat/send": { + "/webchat/conversations/{session_id}": { parameters: { query?: never; header?: never; @@ -1911,27 +1961,22 @@ export interface paths { cookie?: never; }; get?: never; - put?: never; - /** - * Fire-and-forget message injection. The response arrives via the global SSE - * event bus (`/api/events`), same as every other channel. - */ - post: operations["webchat_send"]; - delete?: never; + put: operations["update_webchat_conversation"]; + post?: never; + delete: operations["delete_webchat_conversation"]; options?: never; head?: never; patch?: never; trace?: never; }; - "/workers": { + "/webchat/history": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** List worker runs for an agent, with live status merged from StatusBlocks. */ - get: operations["list_workers"]; + get: operations["webchat_history"]; put?: never; post?: never; delete?: never; @@ -1940,17 +1985,20 @@ export interface paths { patch?: never; trace?: never; }; - "/workers/detail": { + "/webchat/send": { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** Get full detail for a single worker run, including decompressed transcript. */ - get: operations["worker_detail"]; + get?: never; put?: never; - post?: never; + /** + * Fire-and-forget message injection. The response arrives via the global SSE + * event bus (`/api/events`), same as every other channel. + */ + post: operations["webchat_send"]; delete?: never; options?: never; head?: never; @@ -2433,6 +2481,10 @@ export interface components { subtasks?: components["schemas"]["TaskSubtask"][]; title: string; }; + CreateWebChatConversationRequest: { + agent_id: string; + title?: string | null; + }; CreateWorktreeRequest: { agent_id: string; branch: string; @@ -3481,6 +3533,11 @@ export interface components { title?: string | null; worker_id?: string | null; }; + UpdateWebChatConversationRequest: { + agent_id: string; + archived?: boolean | null; + title?: string | null; + }; UploadSkillResponse: { installed: string[]; }; @@ -3531,6 +3588,40 @@ export interface components { /** Format: int64 */ startup_delay_secs?: number | null; }; + WebChatConversation: { + agent_id: string; + archived: boolean; + /** Format: date-time */ + created_at: string; + id: string; + title: string; + title_source: string; + /** Format: date-time */ + updated_at: string; + }; + WebChatConversationResponse: { + conversation: components["schemas"]["WebChatConversation"]; + }; + WebChatConversationSummary: { + agent_id: string; + archived: boolean; + /** Format: date-time */ + created_at: string; + id: string; + /** Format: date-time */ + last_message_at?: string | null; + last_message_preview?: string | null; + last_message_role?: string | null; + /** Format: int64 */ + message_count: number; + title: string; + title_source: string; + /** Format: date-time */ + updated_at: string; + }; + WebChatConversationsResponse: { + conversations: components["schemas"]["WebChatConversationSummary"][]; + }; WebChatHistoryMessage: { content: string; id: string; @@ -5272,6 +5363,86 @@ export interface operations { }; }; }; + list_workers: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + /** @description Maximum number of results to return */ + limit: number; + /** @description Number of results to skip */ + offset: number; + /** @description Filter by worker status */ + status?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WorkerListResponse"]; + }; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + worker_detail: { + parameters: { + query: { + /** @description Agent ID */ + agent_id: string; + /** @description Worker ID */ + worker_id: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WorkerDetailResponse"]; + }; + }; + /** @description Agent or worker not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; list_bindings: { parameters: { query?: { @@ -5867,27 +6038,27 @@ export interface operations { }; }; }; - cortex_chat_threads: { + cortex_chat_delete_thread: { parameters: { - query: { - /** @description Agent ID */ - agent_id: string; - }; + query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + "application/json": components["schemas"]["CortexChatDeleteThreadRequest"]; + }; + }; responses: { - 200: { + /** @description Thread deleted successfully */ + 204: { headers: { [name: string]: unknown; }; - content: { - "application/json": components["schemas"]["CortexChatThreadsResponse"]; - }; + content?: never; }; - /** @description Agent not found */ + /** @description Agent or thread not found */ 404: { headers: { [name: string]: unknown; @@ -5903,27 +6074,27 @@ export interface operations { }; }; }; - cortex_chat_delete_thread: { + cortex_chat_threads: { parameters: { - query?: never; + query: { + /** @description Agent ID */ + agent_id: string; + }; header?: never; path?: never; cookie?: never; }; - requestBody: { - content: { - "application/json": components["schemas"]["CortexChatDeleteThreadRequest"]; - }; - }; + requestBody?: never; responses: { - /** @description Thread deleted successfully */ - 204: { + 200: { headers: { [name: string]: unknown; }; - content?: never; + content: { + "application/json": components["schemas"]["CortexChatThreadsResponse"]; + }; }; - /** @description Agent or thread not found */ + /** @description Agent not found */ 404: { headers: { [name: string]: unknown; @@ -8272,14 +8443,14 @@ export interface operations { }; }; }; - webchat_history: { + list_webchat_conversations: { parameters: { query: { /** @description Agent ID */ agent_id: string; - /** @description Session ID */ - session_id: string; - /** @description Maximum number of messages to return (default: 100, max: 200) */ + /** @description Include archived conversations */ + include_archived: boolean; + /** @description Maximum number of conversations to return (default: 100, max: 500) */ limit: number; }; header?: never; @@ -8293,7 +8464,7 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["WebChatHistoryMessage"][]; + "application/json": components["schemas"]["WebChatConversationsResponse"]; }; }; /** @description Agent not found */ @@ -8312,7 +8483,7 @@ export interface operations { }; }; }; - webchat_send: { + create_webchat_conversation: { parameters: { query?: never; header?: never; @@ -8321,7 +8492,7 @@ export interface operations { }; requestBody: { content: { - "application/json": components["schemas"]["WebChatSendRequest"]; + "application/json": components["schemas"]["CreateWebChatConversationRequest"]; }; }; responses: { @@ -8330,18 +8501,18 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["WebChatSendResponse"]; + "application/json": components["schemas"]["WebChatConversationResponse"]; }; }; - /** @description Invalid request */ - 400: { + /** @description Agent not found */ + 404: { headers: { [name: string]: unknown; }; content?: never; }; - /** @description Messaging manager not available */ - 503: { + /** @description Internal server error */ + 500: { headers: { [name: string]: unknown; }; @@ -8349,20 +8520,57 @@ export interface operations { }; }; }; - list_workers: { + update_webchat_conversation: { + parameters: { + query?: never; + header?: never; + path: { + /** @description Conversation session ID */ + session_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateWebChatConversationRequest"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WebChatConversationResponse"]; + }; + }; + /** @description Conversation not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete_webchat_conversation: { parameters: { query: { /** @description Agent ID */ agent_id: string; - /** @description Maximum number of results to return */ - limit: number; - /** @description Number of results to skip */ - offset: number; - /** @description Filter by worker status */ - status?: string; }; header?: never; - path?: never; + path: { + /** @description Conversation session ID */ + session_id: string; + }; cookie?: never; }; requestBody?: never; @@ -8372,10 +8580,10 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["WorkerListResponse"]; + "application/json": components["schemas"]["WebChatSendResponse"]; }; }; - /** @description Agent not found */ + /** @description Conversation not found */ 404: { headers: { [name: string]: unknown; @@ -8391,13 +8599,15 @@ export interface operations { }; }; }; - worker_detail: { + webchat_history: { parameters: { query: { /** @description Agent ID */ agent_id: string; - /** @description Worker ID */ - worker_id: string; + /** @description Session ID */ + session_id: string; + /** @description Maximum number of messages to return (default: 100, max: 200) */ + limit: number; }; header?: never; path?: never; @@ -8410,10 +8620,10 @@ export interface operations { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["WorkerDetailResponse"]; + "application/json": components["schemas"]["WebChatHistoryMessage"][]; }; }; - /** @description Agent or worker not found */ + /** @description Agent not found */ 404: { headers: { [name: string]: unknown; @@ -8429,4 +8639,48 @@ export interface operations { }; }; }; + webchat_send: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["WebChatSendRequest"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["WebChatSendResponse"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Agent not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Messaging manager not available */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; } diff --git a/interface/src/api/types.ts b/interface/src/api/types.ts index 116e8cc05..b0f079cf6 100644 --- a/interface/src/api/types.ts +++ b/interface/src/api/types.ts @@ -8,7 +8,8 @@ import type { components } from "./schema"; export type StatusResponse = components["schemas"]["StatusResponse"]; export type IdleResponse = components["schemas"]["IdleResponse"]; export type HealthResponse = components["schemas"]["HealthResponse"]; -export type InstanceOverviewResponse = components["schemas"]["InstanceOverviewResponse"]; +export type InstanceOverviewResponse = + components["schemas"]["InstanceOverviewResponse"]; // ============================================================================= // Event/SSE Types @@ -47,14 +48,17 @@ export type MessagesResponse = components["schemas"]["MessagesResponse"]; export type TimelineItem = components["schemas"]["TimelineItem"]; // Status-related -export type CancelProcessRequest = components["schemas"]["CancelProcessRequest"]; -export type CancelProcessResponse = components["schemas"]["CancelProcessResponse"]; +export type CancelProcessRequest = + components["schemas"]["CancelProcessRequest"]; +export type CancelProcessResponse = + components["schemas"]["CancelProcessResponse"]; // Prompt inspection export type PromptCaptureBody = components["schemas"]["PromptCaptureBody"]; // Archive -export type SetChannelArchiveRequest = components["schemas"]["SetChannelArchiveRequest"]; +export type SetChannelArchiveRequest = + components["schemas"]["SetChannelArchiveRequest"]; // ============================================================================= // Worker Types @@ -62,7 +66,8 @@ export type SetChannelArchiveRequest = components["schemas"]["SetChannelArchiveR export type WorkerListItem = components["schemas"]["WorkerListItem"]; export type WorkerListResponse = components["schemas"]["WorkerListResponse"]; -export type WorkerDetailResponse = components["schemas"]["WorkerDetailResponse"]; +export type WorkerDetailResponse = + components["schemas"]["WorkerDetailResponse"]; // Transcript export type ActionContent = components["schemas"]["ActionContent"]; @@ -75,9 +80,11 @@ export type TranscriptStep = components["schemas"]["TranscriptStep"]; export type AgentInfo = components["schemas"]["AgentInfo"]; export type AgentsResponse = components["schemas"]["AgentsResponse"]; export type AgentSummary = components["schemas"]["AgentSummary"]; -export type AgentOverviewResponse = components["schemas"]["AgentOverviewResponse"]; +export type AgentOverviewResponse = + components["schemas"]["AgentOverviewResponse"]; export type AgentProfile = components["schemas"]["AgentProfile"]; -export type AgentProfileResponse = components["schemas"]["AgentProfileResponse"]; +export type AgentProfileResponse = + components["schemas"]["AgentProfileResponse"]; // CRUD export type CreateAgentRequest = components["schemas"]["CreateAgentRequest"]; @@ -88,10 +95,28 @@ export type WarmupSection = components["schemas"]["WarmupSection"]; export type WarmupUpdate = components["schemas"]["WarmupUpdate"]; export type WarmupStatus = components["schemas"]["WarmupStatus"]; export type WarmupStatusEntry = components["schemas"]["WarmupStatusEntry"]; -export type WarmupStatusResponse = components["schemas"]["WarmupStatusResponse"]; +export type WarmupStatusResponse = + components["schemas"]["WarmupStatusResponse"]; export type WarmupState = components["schemas"]["WarmupState"]; -export type WarmupTriggerRequest = components["schemas"]["WarmupTriggerRequest"]; -export type WarmupTriggerResponse = components["schemas"]["WarmupTriggerResponse"]; +export type WarmupTriggerRequest = + components["schemas"]["WarmupTriggerRequest"]; +export type WarmupTriggerResponse = + components["schemas"]["WarmupTriggerResponse"]; + +// Webchat conversations +export type WebChatConversation = components["schemas"]["WebChatConversation"]; +export type WebChatConversationSummary = + components["schemas"]["WebChatConversationSummary"]; +export type WebChatConversationsResponse = + components["schemas"]["WebChatConversationsResponse"]; +export type WebChatConversationResponse = + components["schemas"]["WebChatConversationResponse"]; +export type CreateWebChatConversationRequest = + components["schemas"]["CreateWebChatConversationRequest"]; +export type UpdateWebChatConversationRequest = + components["schemas"]["UpdateWebChatConversationRequest"]; +export type WebChatHistoryMessage = + components["schemas"]["WebChatHistoryMessage"]; // Activity export type ActivityDayCount = components["schemas"]["ActivityDayCount"]; @@ -104,40 +129,49 @@ export type HeatmapCell = components["schemas"]["HeatmapCell"]; export type Memory = components["schemas"]["Memory"]; export type MemoryType = components["schemas"]["MemoryType"]; -export type MemoriesListResponse = components["schemas"]["MemoriesListResponse"]; +export type MemoriesListResponse = + components["schemas"]["MemoriesListResponse"]; // Search export type MemorySearchResult = components["schemas"]["MemorySearchResult"]; -export type MemoriesSearchResponse = components["schemas"]["MemoriesSearchResponse"]; +export type MemoriesSearchResponse = + components["schemas"]["MemoriesSearchResponse"]; // Graph export type Association = components["schemas"]["Association"]; export type RelationType = components["schemas"]["RelationType"]; export type MemoryGraphResponse = components["schemas"]["MemoryGraphResponse"]; -export type MemoryGraphNeighborsResponse = components["schemas"]["MemoryGraphNeighborsResponse"]; +export type MemoryGraphNeighborsResponse = + components["schemas"]["MemoryGraphNeighborsResponse"]; // ============================================================================= // Cortex Types // ============================================================================= export type CortexEvent = components["schemas"]["CortexEvent"]; -export type CortexEventsResponse = components["schemas"]["CortexEventsResponse"]; +export type CortexEventsResponse = + components["schemas"]["CortexEventsResponse"]; // Chat export type CortexChatMessage = components["schemas"]["CortexChatMessage"]; -export type CortexChatMessagesResponse = components["schemas"]["CortexChatMessagesResponse"]; +export type CortexChatMessagesResponse = + components["schemas"]["CortexChatMessagesResponse"]; export type CortexChatThread = components["schemas"]["CortexChatThread"]; -export type CortexChatThreadsResponse = components["schemas"]["CortexChatThreadsResponse"]; +export type CortexChatThreadsResponse = + components["schemas"]["CortexChatThreadsResponse"]; export type CortexChatToolCall = components["schemas"]["CortexChatToolCall"]; -export type CortexChatSendRequest = components["schemas"]["CortexChatSendRequest"]; -export type CortexChatDeleteThreadRequest = components["schemas"]["CortexChatDeleteThreadRequest"]; +export type CortexChatSendRequest = + components["schemas"]["CortexChatSendRequest"]; +export type CortexChatDeleteThreadRequest = + components["schemas"]["CortexChatDeleteThreadRequest"]; // ============================================================================= // Config Types // ============================================================================= export type AgentConfigResponse = components["schemas"]["AgentConfigResponse"]; -export type AgentConfigUpdateRequest = components["schemas"]["AgentConfigUpdateRequest"]; +export type AgentConfigUpdateRequest = + components["schemas"]["AgentConfigUpdateRequest"]; // Sections export type RoutingSection = components["schemas"]["RoutingSection"]; @@ -145,7 +179,8 @@ export type TuningSection = components["schemas"]["TuningSection"]; export type CompactionSection = components["schemas"]["CompactionSection"]; export type CortexSection = components["schemas"]["CortexSection"]; export type CoalesceSection = components["schemas"]["CoalesceSection"]; -export type MemoryPersistenceSection = components["schemas"]["MemoryPersistenceSection"]; +export type MemoryPersistenceSection = + components["schemas"]["MemoryPersistenceSection"]; export type BrowserSection = components["schemas"]["BrowserSection"]; export type ChannelSection = components["schemas"]["ChannelSection"]; export type SandboxSection = components["schemas"]["SandboxSection"]; @@ -158,7 +193,8 @@ export type TuningUpdate = components["schemas"]["TuningUpdate"]; export type CompactionUpdate = components["schemas"]["CompactionUpdate"]; export type CortexUpdate = components["schemas"]["CortexUpdate"]; export type CoalesceUpdate = components["schemas"]["CoalesceUpdate"]; -export type MemoryPersistenceUpdate = components["schemas"]["MemoryPersistenceUpdate"]; +export type MemoryPersistenceUpdate = + components["schemas"]["MemoryPersistenceUpdate"]; export type BrowserUpdate = components["schemas"]["BrowserUpdate"]; export type ChannelUpdate = components["schemas"]["ChannelUpdate"]; export type SandboxUpdate = components["schemas"]["SandboxUpdate"]; @@ -169,20 +205,29 @@ export type DiscordUpdate = components["schemas"]["DiscordUpdate"]; export type ClosePolicy = components["schemas"]["ClosePolicy"]; // Global settings -export type GlobalSettingsResponse = components["schemas"]["GlobalSettingsResponse"]; -export type GlobalSettingsUpdate = components["schemas"]["GlobalSettingsUpdate"]; -export type GlobalSettingsUpdateResponse = components["schemas"]["GlobalSettingsUpdateResponse"]; +export type GlobalSettingsResponse = + components["schemas"]["GlobalSettingsResponse"]; +export type GlobalSettingsUpdate = + components["schemas"]["GlobalSettingsUpdate"]; +export type GlobalSettingsUpdateResponse = + components["schemas"]["GlobalSettingsUpdateResponse"]; // OpenCode (within global settings) -export type OpenCodeSettingsResponse = components["schemas"]["OpenCodeSettingsResponse"]; -export type OpenCodeSettingsUpdate = components["schemas"]["OpenCodeSettingsUpdate"]; -export type OpenCodePermissionsResponse = components["schemas"]["OpenCodePermissionsResponse"]; -export type OpenCodePermissionsUpdate = components["schemas"]["OpenCodePermissionsUpdate"]; +export type OpenCodeSettingsResponse = + components["schemas"]["OpenCodeSettingsResponse"]; +export type OpenCodeSettingsUpdate = + components["schemas"]["OpenCodeSettingsUpdate"]; +export type OpenCodePermissionsResponse = + components["schemas"]["OpenCodePermissionsResponse"]; +export type OpenCodePermissionsUpdate = + components["schemas"]["OpenCodePermissionsUpdate"]; // Raw config export type RawConfigResponse = components["schemas"]["RawConfigResponse"]; -export type RawConfigUpdateRequest = components["schemas"]["RawConfigUpdateRequest"]; -export type RawConfigUpdateResponse = components["schemas"]["RawConfigUpdateResponse"]; +export type RawConfigUpdateRequest = + components["schemas"]["RawConfigUpdateRequest"]; +export type RawConfigUpdateResponse = + components["schemas"]["RawConfigUpdateResponse"]; // ============================================================================= // Cron Types @@ -192,7 +237,8 @@ export type CronJobInfo = components["schemas"]["CronJobInfo"]; export type CronJobWithStats = components["schemas"]["CronJobWithStats"]; export type CronListResponse = components["schemas"]["CronListResponse"]; export type CronExecutionEntry = components["schemas"]["CronExecutionEntry"]; -export type CronExecutionsResponse = components["schemas"]["CronExecutionsResponse"]; +export type CronExecutionsResponse = + components["schemas"]["CronExecutionsResponse"]; export type CronActionResponse = components["schemas"]["CronActionResponse"]; // Requests @@ -206,17 +252,24 @@ export type TriggerCronRequest = components["schemas"]["TriggerCronRequest"]; export type ProviderStatus = components["schemas"]["ProviderStatus"]; export type ProvidersResponse = components["schemas"]["ProvidersResponse"]; -export type ProviderUpdateRequest = components["schemas"]["ProviderUpdateRequest"]; -export type ProviderUpdateResponse = components["schemas"]["ProviderUpdateResponse"]; +export type ProviderUpdateRequest = + components["schemas"]["ProviderUpdateRequest"]; +export type ProviderUpdateResponse = + components["schemas"]["ProviderUpdateResponse"]; // Model testing -export type ProviderModelTestRequest = components["schemas"]["ProviderModelTestRequest"]; -export type ProviderModelTestResponse = components["schemas"]["ProviderModelTestResponse"]; +export type ProviderModelTestRequest = + components["schemas"]["ProviderModelTestRequest"]; +export type ProviderModelTestResponse = + components["schemas"]["ProviderModelTestResponse"]; // OAuth -export type OpenAiOAuthBrowserStartRequest = components["schemas"]["OpenAiOAuthBrowserStartRequest"]; -export type OpenAiOAuthBrowserStartResponse = components["schemas"]["OpenAiOAuthBrowserStartResponse"]; -export type OpenAiOAuthBrowserStatusResponse = components["schemas"]["OpenAiOAuthBrowserStatusResponse"]; +export type OpenAiOAuthBrowserStartRequest = + components["schemas"]["OpenAiOAuthBrowserStartRequest"]; +export type OpenAiOAuthBrowserStartResponse = + components["schemas"]["OpenAiOAuthBrowserStartResponse"]; +export type OpenAiOAuthBrowserStatusResponse = + components["schemas"]["OpenAiOAuthBrowserStatusResponse"]; // Models export type ModelInfo = components["schemas"]["ModelInfo"]; @@ -228,8 +281,10 @@ export type ModelsResponse = components["schemas"]["ModelsResponse"]; export type IngestFileInfo = components["schemas"]["IngestFileInfo"]; export type IngestFilesResponse = components["schemas"]["IngestFilesResponse"]; -export type IngestUploadResponse = components["schemas"]["IngestUploadResponse"]; -export type IngestDeleteResponse = components["schemas"]["IngestDeleteResponse"]; +export type IngestUploadResponse = + components["schemas"]["IngestUploadResponse"]; +export type IngestDeleteResponse = + components["schemas"]["IngestDeleteResponse"]; // ============================================================================= // Skills Types @@ -237,20 +292,25 @@ export type IngestDeleteResponse = components["schemas"]["IngestDeleteResponse"] export type SkillInfo = components["schemas"]["SkillInfo"]; export type SkillsListResponse = components["schemas"]["SkillsListResponse"]; -export type SkillContentResponse = components["schemas"]["SkillContentResponse"]; +export type SkillContentResponse = + components["schemas"]["SkillContentResponse"]; // Install/Remove export type InstallSkillRequest = components["schemas"]["InstallSkillRequest"]; -export type InstallSkillResponse = components["schemas"]["InstallSkillResponse"]; +export type InstallSkillResponse = + components["schemas"]["InstallSkillResponse"]; export type RemoveSkillRequest = components["schemas"]["RemoveSkillRequest"]; export type RemoveSkillResponse = components["schemas"]["RemoveSkillResponse"]; export type UploadSkillResponse = components["schemas"]["UploadSkillResponse"]; // Registry export type RegistrySkill = components["schemas"]["RegistrySkill"]; -export type RegistryBrowseResponse = components["schemas"]["RegistryBrowseResponse"]; -export type RegistrySearchResponse = components["schemas"]["RegistrySearchResponse"]; -export type RegistrySkillContentResponse = components["schemas"]["RegistrySkillContentResponse"]; +export type RegistryBrowseResponse = + components["schemas"]["RegistryBrowseResponse"]; +export type RegistrySearchResponse = + components["schemas"]["RegistrySearchResponse"]; +export type RegistrySkillContentResponse = + components["schemas"]["RegistrySkillContentResponse"]; // ============================================================================= // Tasks Types @@ -275,34 +335,47 @@ export type AssignRequest = components["schemas"]["AssignRequest"]; // ============================================================================= export type PlatformStatus = components["schemas"]["PlatformStatus"]; -export type AdapterInstanceStatus = components["schemas"]["AdapterInstanceStatus"]; -export type MessagingStatusResponse = components["schemas"]["MessagingStatusResponse"]; -export type MessagingInstanceActionResponse = components["schemas"]["MessagingInstanceActionResponse"]; +export type AdapterInstanceStatus = + components["schemas"]["AdapterInstanceStatus"]; +export type MessagingStatusResponse = + components["schemas"]["MessagingStatusResponse"]; +export type MessagingInstanceActionResponse = + components["schemas"]["MessagingInstanceActionResponse"]; // Instances -export type CreateMessagingInstanceRequest = components["schemas"]["CreateMessagingInstanceRequest"]; -export type DeleteMessagingInstanceRequest = components["schemas"]["DeleteMessagingInstanceRequest"]; +export type CreateMessagingInstanceRequest = + components["schemas"]["CreateMessagingInstanceRequest"]; +export type DeleteMessagingInstanceRequest = + components["schemas"]["DeleteMessagingInstanceRequest"]; export type InstanceCredentials = components["schemas"]["InstanceCredentials"]; // Bindings export type BindingResponse = components["schemas"]["BindingResponse"]; -export type BindingsListResponse = components["schemas"]["BindingsListResponse"]; -export type CreateBindingRequest = components["schemas"]["CreateBindingRequest"]; -export type CreateBindingResponse = components["schemas"]["CreateBindingResponse"]; -export type UpdateBindingRequest = components["schemas"]["UpdateBindingRequest"]; -export type UpdateBindingResponse = components["schemas"]["UpdateBindingResponse"]; -export type DeleteBindingRequest = components["schemas"]["DeleteBindingRequest"]; -export type DeleteBindingResponse = components["schemas"]["DeleteBindingResponse"]; +export type BindingsListResponse = + components["schemas"]["BindingsListResponse"]; +export type CreateBindingRequest = + components["schemas"]["CreateBindingRequest"]; +export type CreateBindingResponse = + components["schemas"]["CreateBindingResponse"]; +export type UpdateBindingRequest = + components["schemas"]["UpdateBindingRequest"]; +export type UpdateBindingResponse = + components["schemas"]["UpdateBindingResponse"]; +export type DeleteBindingRequest = + components["schemas"]["DeleteBindingRequest"]; +export type DeleteBindingResponse = + components["schemas"]["DeleteBindingResponse"]; export type PlatformCredentials = components["schemas"]["PlatformCredentials"]; // Toggles -export type TogglePlatformRequest = components["schemas"]["TogglePlatformRequest"]; -export type DisconnectPlatformRequest = components["schemas"]["DisconnectPlatformRequest"]; +export type TogglePlatformRequest = + components["schemas"]["TogglePlatformRequest"]; +export type DisconnectPlatformRequest = + components["schemas"]["DisconnectPlatformRequest"]; // Web chat export type WebChatSendRequest = components["schemas"]["WebChatSendRequest"]; export type WebChatSendResponse = components["schemas"]["WebChatSendResponse"]; -export type WebChatHistoryMessage = components["schemas"]["WebChatHistoryMessage"]; // ============================================================================= // Links Types @@ -334,8 +407,10 @@ export type Project = components["schemas"]["Project"]; export type ProjectStatus = components["schemas"]["ProjectStatus"]; export type ProjectRepo = components["schemas"]["ProjectRepo"]; export type ProjectWorktree = components["schemas"]["ProjectWorktree"]; -export type ProjectWorktreeWithRepo = components["schemas"]["ProjectWorktreeWithRepo"]; -export type ProjectWithRelations = components["schemas"]["ProjectWithRelations"]; +export type ProjectWorktreeWithRepo = + components["schemas"]["ProjectWorktreeWithRepo"]; +export type ProjectWithRelations = + components["schemas"]["ProjectWithRelations"]; export type ProjectListResponse = components["schemas"]["ProjectListResponse"]; export type ProjectResponse = components["schemas"]["ProjectResponse"]; @@ -344,10 +419,13 @@ export type DiskUsageEntry = components["schemas"]["DiskUsageEntry"]; export type DiskUsageResponse = components["schemas"]["DiskUsageResponse"]; // CRUD -export type CreateProjectRequest = components["schemas"]["CreateProjectRequest"]; -export type UpdateProjectRequest = components["schemas"]["UpdateProjectRequest"]; +export type CreateProjectRequest = + components["schemas"]["CreateProjectRequest"]; +export type UpdateProjectRequest = + components["schemas"]["UpdateProjectRequest"]; export type CreateRepoRequest = components["schemas"]["CreateRepoRequest"]; -export type CreateWorktreeRequest = components["schemas"]["CreateWorktreeRequest"]; +export type CreateWorktreeRequest = + components["schemas"]["CreateWorktreeRequest"]; // Responses export type RepoResponse = components["schemas"]["RepoResponse"]; @@ -366,7 +444,8 @@ export type SecretInfoResponse = components["schemas"]["SecretInfoResponse"]; // CRUD export type PutSecretBody = components["schemas"]["PutSecretBody"]; export type PutSecretResponse = components["schemas"]["PutSecretResponse"]; -export type DeleteSecretResponse = components["schemas"]["DeleteSecretResponse"]; +export type DeleteSecretResponse = + components["schemas"]["DeleteSecretResponse"]; // Encryption export type EncryptResponse = components["schemas"]["EncryptResponse"]; diff --git a/justfile b/justfile index 5c26447e2..1958d272b 100644 --- a/justfile +++ b/justfile @@ -36,6 +36,10 @@ check-typegen: cd interface && bunx openapi-typescript /tmp/spacebot-openapi-check.json -o /tmp/spacebot-schema-check.d.ts diff interface/src/api/schema.d.ts /tmp/spacebot-schema-check.d.ts +typegen-package: + cargo run --bin openapi-spec > /tmp/spacebot-openapi-package.json + cd interface && bunx openapi-typescript /tmp/spacebot-openapi-package.json -o src/api/schema.d.ts + gate-pr-ci: preflight-ci ./scripts/gate-pr.sh --ci diff --git a/migrations/20260324000001_webchat_conversations.sql b/migrations/20260324000001_webchat_conversations.sql new file mode 100644 index 000000000..3a206f46c --- /dev/null +++ b/migrations/20260324000001_webchat_conversations.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS webchat_conversations ( + id TEXT PRIMARY KEY, + agent_id TEXT NOT NULL, + title TEXT NOT NULL, + title_source TEXT NOT NULL DEFAULT 'system', + archived INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX IF NOT EXISTS idx_webchat_conversations_agent_updated + ON webchat_conversations(agent_id, archived, updated_at DESC); diff --git a/packages/api-client/package.json b/packages/api-client/package.json new file mode 100644 index 000000000..a87d65c9a --- /dev/null +++ b/packages/api-client/package.json @@ -0,0 +1,13 @@ +{ + "name": "@spacebot/api-client", + "version": "0.1.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts", + "./client": "./src/client.ts", + "./types": "./src/types.ts", + "./schema": "./src/schema.d.ts", + "./events": "./src/events.ts" + } +} diff --git a/packages/api-client/src/client.ts b/packages/api-client/src/client.ts new file mode 100644 index 000000000..0f7535b98 --- /dev/null +++ b/packages/api-client/src/client.ts @@ -0,0 +1,235 @@ +import type { + WebChatConversationResponse, + WebChatConversationsResponse, + WebChatHistoryMessage, + HealthResponse, + MemoriesListResponse, + MessagesResponse, + StatusResponse, + TaskListResponse, + TimelineItem, + WorkerDetailResponse, + WorkerListResponse, + WarmupStatusResponse, +} from "./types"; + +let baseUrl = ""; +let authToken: string | null = null; + +export function setServerUrl(url: string) { + baseUrl = url.replace(/\/+$/, ""); +} + +export function getServerUrl() { + return baseUrl; +} + +export function setAuthToken(token: string | null | undefined) { + authToken = token?.trim() || null; +} + +export function getAuthToken() { + return authToken; +} + +export function getApiBase() { + return baseUrl ? `${baseUrl}/api` : "/api"; +} + +export function getEventsUrl() { + return `${getApiBase()}/events`; +} + +function buildHeaders(extra?: HeadersInit): HeadersInit { + return { + "Content-Type": "application/json", + ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}), + ...(extra ?? {}), + }; +} + +async function request(path: string, init?: RequestInit): Promise { + const response = await fetch(`${getApiBase()}${path}`, { + ...init, + headers: buildHeaders(init?.headers), + }); + + if (!response.ok) { + throw new Error(`Spacebot API error: ${response.status}`); + } + + return response.json() as Promise; +} + +export const apiClient = { + health() { + return request("/health"); + }, + + status() { + return request("/status"); + }, + + warmup(agentId: string) { + return request( + `/agents/warmup?agent_id=${encodeURIComponent(agentId)}`, + ); + }, + + webchatHistory(agentId: string, sessionId: string, limit = 100) { + const params = new URLSearchParams({ + agent_id: agentId, + session_id: sessionId, + limit: String(limit), + }); + return request( + `/webchat/history?${params.toString()}`, + ); + }, + + channelMessages(channelId: string, limit = 200, before?: string) { + const params = new URLSearchParams({ + channel_id: channelId, + limit: String(limit), + }); + + if (before) { + params.set('before', before); + } + + return request(`/channels/messages?${params.toString()}`); + }, + + webchatSend(input: { + agentId: string; + sessionId: string; + senderName?: string; + message: string; + }) { + return request<{ ok: boolean }>("/webchat/send", { + method: "POST", + body: JSON.stringify({ + agent_id: input.agentId, + session_id: input.sessionId, + sender_name: input.senderName ?? "user", + message: input.message, + }), + }); + }, + + listWebchatConversations( + agentId: string, + includeArchived = false, + limit = 100, + ) { + const params = new URLSearchParams({ + agent_id: agentId, + include_archived: includeArchived ? "true" : "false", + limit: String(limit), + }); + return request( + `/webchat/conversations?${params.toString()}`, + ); + }, + + createWebchatConversation(input: { agentId: string; title?: string | null }) { + return request("/webchat/conversations", { + method: "POST", + body: JSON.stringify({ + agent_id: input.agentId, + title: input.title ?? undefined, + }), + }); + }, + + updateWebchatConversation(input: { + agentId: string; + sessionId: string; + title?: string | null; + archived?: boolean; + }) { + return request( + `/webchat/conversations/${encodeURIComponent(input.sessionId)}`, + { + method: "PUT", + body: JSON.stringify({ + agent_id: input.agentId, + title: input.title ?? undefined, + archived: input.archived, + }), + }, + ); + }, + + deleteWebchatConversation(agentId: string, sessionId: string) { + const params = new URLSearchParams({ agent_id: agentId }); + return request<{ ok: boolean }>( + `/webchat/conversations/${encodeURIComponent(sessionId)}?${params.toString()}`, + { + method: "DELETE", + }, + ); + }, + + listTasks(agentId: string, limit = 20) { + const params = new URLSearchParams({ + agent_id: agentId, + limit: String(limit), + }); + return request(`/tasks?${params.toString()}`); + }, + + listMemories(agentId: string, limit = 12, sort = "recent") { + const params = new URLSearchParams({ + agent_id: agentId, + limit: String(limit), + sort, + }); + return request( + `/agents/memories?${params.toString()}`, + ); + }, + + listWorkers(input: { + agentId: string; + limit?: number; + offset?: number; + status?: string; + }) { + const params = new URLSearchParams({ + agent_id: input.agentId, + limit: String(input.limit ?? 50), + offset: String(input.offset ?? 0), + }); + + if (input.status) { + params.set("status", input.status); + } + + return request(`/agents/workers?${params.toString()}`); + }, + + workerDetail(agentId: string, workerId: string) { + const params = new URLSearchParams({ + agent_id: agentId, + worker_id: workerId, + }); + + return request(`/agents/workers/detail?${params.toString()}`); + }, + + cancelProcess(input: { + channelId: string; + processType: "worker" | "branch"; + processId: string; + }) { + return request<{ success: boolean; message: string }>("/channels/cancel-process", { + method: "POST", + body: JSON.stringify({ + channel_id: input.channelId, + process_type: input.processType, + process_id: input.processId, + }), + }); + }, +}; diff --git a/packages/api-client/src/events.ts b/packages/api-client/src/events.ts new file mode 100644 index 000000000..e330734c8 --- /dev/null +++ b/packages/api-client/src/events.ts @@ -0,0 +1,155 @@ +import type { CortexChatToolCall } from "./types"; + +export type ProcessType = "channel" | "branch" | "worker"; + +export interface InboundMessageEvent { + type: "inbound_message"; + agent_id: string; + channel_id: string; + sender_name?: string | null; + sender_id: string; + text: string; +} + +export interface OutboundMessageEvent { + type: "outbound_message"; + agent_id: string; + channel_id: string; + text: string; +} + +export interface OutboundMessageDeltaEvent { + type: "outbound_message_delta"; + agent_id: string; + channel_id: string; + text_delta: string; + aggregated_text: string; +} + +export interface TypingStateEvent { + type: "typing_state"; + agent_id: string; + channel_id: string; + is_typing: boolean; +} + +export interface WorkerStartedEvent { + type: "worker_started"; + agent_id: string; + channel_id: string | null; + worker_id: string; + task: string; + worker_type?: string; + interactive?: boolean; +} + +export interface WorkerStatusEvent { + type: "worker_status"; + agent_id: string; + channel_id: string | null; + worker_id: string; + status: string; +} + +export interface WorkerIdleEvent { + type: "worker_idle"; + agent_id: string; + channel_id: string | null; + worker_id: string; +} + +export interface WorkerCompletedEvent { + type: "worker_completed"; + agent_id: string; + channel_id: string | null; + worker_id: string; + result: string; + success?: boolean; +} + +export interface BranchStartedEvent { + type: "branch_started"; + agent_id: string; + channel_id: string; + branch_id: string; + description: string; +} + +export interface BranchCompletedEvent { + type: "branch_completed"; + agent_id: string; + channel_id: string; + branch_id: string; + conclusion: string; +} + +export interface ToolStartedEvent { + type: "tool_started"; + agent_id: string; + channel_id: string | null; + process_type: ProcessType; + process_id: string; + tool_name: string; + args: string; +} + +export interface ToolCompletedEvent { + type: "tool_completed"; + agent_id: string; + channel_id: string | null; + process_type: ProcessType; + process_id: string; + tool_name: string; + result: string; +} + +export type OpenCodeToolState = + | { status: "pending" } + | { status: "running"; title?: string; input?: string } + | { status: "completed"; title?: string; input?: string; output?: string } + | { status: "error"; error?: string }; + +export type OpenCodePart = + | { type: "text"; id: string; text: string } + | ({ type: "tool"; id: string; tool: string } & OpenCodeToolState) + | { type: "step_start"; id: string } + | { type: "step_finish"; id: string; reason?: string }; + +export interface OpenCodePartUpdatedEvent { + type: "opencode_part_updated"; + agent_id: string; + worker_id: string; + part: OpenCodePart; +} + +export interface WorkerTextEvent { + type: "worker_text"; + agent_id: string; + worker_id: string; + text: string; +} + +export interface CortexChatMessageEvent { + type: "cortex_chat_message"; + agent_id: string; + thread_id: string; + content: string; + tool_calls?: CortexChatToolCall[]; +} + +export type ApiEvent = + | InboundMessageEvent + | OutboundMessageEvent + | OutboundMessageDeltaEvent + | TypingStateEvent + | WorkerStartedEvent + | WorkerStatusEvent + | WorkerIdleEvent + | WorkerCompletedEvent + | BranchStartedEvent + | BranchCompletedEvent + | ToolStartedEvent + | ToolCompletedEvent + | OpenCodePartUpdatedEvent + | WorkerTextEvent + | CortexChatMessageEvent; diff --git a/packages/api-client/src/index.ts b/packages/api-client/src/index.ts new file mode 100644 index 000000000..2634183ad --- /dev/null +++ b/packages/api-client/src/index.ts @@ -0,0 +1,3 @@ +export * from "./client"; +export * from "./types"; +export * from "./events"; diff --git a/packages/api-client/src/schema.d.ts b/packages/api-client/src/schema.d.ts new file mode 100644 index 000000000..ebb621110 --- /dev/null +++ b/packages/api-client/src/schema.d.ts @@ -0,0 +1 @@ +export * from "../../../interface/src/api/schema"; diff --git a/packages/api-client/src/types.ts b/packages/api-client/src/types.ts new file mode 100644 index 000000000..3da31b9eb --- /dev/null +++ b/packages/api-client/src/types.ts @@ -0,0 +1 @@ +export * from "../../../interface/src/api/types"; diff --git a/src/agent/channel.rs b/src/agent/channel.rs index 457ee42d1..c5af1725c 100644 --- a/src/agent/channel.rs +++ b/src/agent/channel.rs @@ -2415,7 +2415,10 @@ impl Channel { // ── Prompt snapshot capture (fire-and-forget) ── self.maybe_capture_snapshot(system_prompt, user_text, &history); - let mut result = self.hook.prompt_once(&agent, &mut history, user_text).await; + let mut result = self + .hook + .prompt_once_streaming(&agent, &mut history, user_text, max_turns) + .await; // If the LLM responded with text that looks like tool call syntax, it failed // to use the tool calling API. Inject a correction and retry a couple @@ -2440,7 +2443,7 @@ impl Channel { let correction = prompt_engine.render_system_tool_syntax_correction()?; result = self .hook - .prompt_once(&agent, &mut history, &correction) + .prompt_once_streaming(&agent, &mut history, &correction, max_turns) .await; } diff --git a/src/api/server.rs b/src/api/server.rs index 1db3a7f67..ff84ff2a2 100644 --- a/src/api/server.rs +++ b/src/api/server.rs @@ -217,6 +217,10 @@ pub fn api_router() -> OpenApiRouter> { // Webchat routes .routes(routes!(webchat::webchat_send)) .routes(routes!(webchat::webchat_history)) + .routes(routes!(webchat::list_webchat_conversations)) + .routes(routes!(webchat::create_webchat_conversation)) + .routes(routes!(webchat::update_webchat_conversation)) + .routes(routes!(webchat::delete_webchat_conversation)) // Link routes .routes(routes!(links::list_links, links::create_link)) .routes(routes!(links::update_link, links::delete_link)) diff --git a/src/api/webchat.rs b/src/api/webchat.rs index 8a7c4873a..7a5f6569d 100644 --- a/src/api/webchat.rs +++ b/src/api/webchat.rs @@ -1,8 +1,11 @@ use super::state::ApiState; -use crate::{InboundMessage, MessageContent}; +use crate::{ + InboundMessage, MessageContent, + conversation::{WebChatConversation, WebChatConversationStore, WebChatConversationSummary}, +}; use axum::Json; -use axum::extract::{Query, State}; +use axum::extract::{Path, Query, State}; use axum::http::StatusCode; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -26,6 +29,75 @@ pub(super) struct WebChatSendResponse { ok: bool, } +#[derive(Deserialize, utoipa::ToSchema, utoipa::IntoParams)] +pub(super) struct WebChatHistoryQuery { + agent_id: String, + session_id: String, + #[serde(default = "default_limit")] + limit: i64, +} + +fn default_limit() -> i64 { + 100 +} + +#[derive(Serialize, utoipa::ToSchema)] +pub(super) struct WebChatHistoryMessage { + id: String, + role: String, + content: String, +} + +#[derive(Deserialize, utoipa::ToSchema, utoipa::IntoParams)] +pub(super) struct WebChatConversationsQuery { + agent_id: String, + #[serde(default)] + include_archived: bool, + #[serde(default = "default_conversation_limit")] + limit: i64, +} + +fn default_conversation_limit() -> i64 { + 100 +} + +#[derive(Serialize, utoipa::ToSchema)] +pub(super) struct WebChatConversationsResponse { + conversations: Vec, +} + +#[derive(Deserialize, utoipa::ToSchema)] +pub(super) struct CreateWebChatConversationRequest { + agent_id: String, + title: Option, +} + +#[derive(Serialize, utoipa::ToSchema)] +pub(super) struct WebChatConversationResponse { + conversation: WebChatConversation, +} + +#[derive(Deserialize, utoipa::ToSchema)] +pub(super) struct UpdateWebChatConversationRequest { + agent_id: String, + title: Option, + archived: Option, +} + +#[derive(Deserialize, utoipa::ToSchema, utoipa::IntoParams)] +pub(super) struct DeleteWebChatConversationQuery { + agent_id: String, +} + +fn conversation_store( + state: &Arc, + agent_id: &str, +) -> Result { + let pools = state.agent_pools.load(); + let pool = pools.get(agent_id).ok_or(StatusCode::NOT_FOUND)?; + Ok(WebChatConversationStore::new(pool.clone())) +} + /// Fire-and-forget message injection. The response arrives via the global SSE /// event bus (`/api/events`), same as every other channel. #[utoipa::path( @@ -35,6 +107,7 @@ pub(super) struct WebChatSendResponse { responses( (status = 200, body = WebChatSendResponse), (status = 400, description = "Invalid request"), + (status = 404, description = "Agent not found"), (status = 503, description = "Messaging manager not available"), ), tag = "webchat", @@ -50,6 +123,22 @@ pub(super) async fn webchat_send( .clone() .ok_or(StatusCode::SERVICE_UNAVAILABLE)?; + let store = conversation_store(&state, &request.agent_id)?; + store + .ensure(&request.agent_id, &request.session_id) + .await + .map_err(|error| { + tracing::warn!(%error, session_id = %request.session_id, "failed to ensure webchat conversation"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + store + .maybe_set_generated_title(&request.agent_id, &request.session_id, &request.message) + .await + .map_err(|error| { + tracing::warn!(%error, session_id = %request.session_id, "failed to update generated webchat title"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + let conversation_id = request.session_id.clone(); let mut metadata = HashMap::new(); @@ -79,25 +168,6 @@ pub(super) async fn webchat_send( Ok(Json(WebChatSendResponse { ok: true })) } -#[derive(Deserialize, utoipa::ToSchema, utoipa::IntoParams)] -pub(super) struct WebChatHistoryQuery { - agent_id: String, - session_id: String, - #[serde(default = "default_limit")] - limit: i64, -} - -fn default_limit() -> i64 { - 100 -} - -#[derive(Serialize, utoipa::ToSchema)] -pub(super) struct WebChatHistoryMessage { - id: String, - role: String, - content: String, -} - #[utoipa::path( get, path = "/webchat/history", @@ -133,12 +203,142 @@ pub(super) async fn webchat_history( let result: Vec = messages .into_iter() - .map(|m| WebChatHistoryMessage { - id: m.id, - role: m.role, - content: m.content, + .map(|message| WebChatHistoryMessage { + id: message.id, + role: message.role, + content: message.content, }) .collect(); Ok(Json(result)) } + +#[utoipa::path( + get, + path = "/webchat/conversations", + params( + ("agent_id" = String, Query, description = "Agent ID"), + ("include_archived" = bool, Query, description = "Include archived conversations"), + ("limit" = i64, Query, description = "Maximum number of conversations to return (default: 100, max: 500)"), + ), + responses( + (status = 200, body = WebChatConversationsResponse), + (status = 404, description = "Agent not found"), + (status = 500, description = "Internal server error"), + ), + tag = "webchat", +)] +pub(super) async fn list_webchat_conversations( + State(state): State>, + Query(query): Query, +) -> Result, StatusCode> { + let store = conversation_store(&state, &query.agent_id)?; + let conversations = store + .list(&query.agent_id, query.include_archived, query.limit) + .await + .map_err(|error| { + tracing::warn!(%error, agent_id = %query.agent_id, "failed to list webchat conversations"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + Ok(Json(WebChatConversationsResponse { conversations })) +} + +#[utoipa::path( + post, + path = "/webchat/conversations", + request_body = CreateWebChatConversationRequest, + responses( + (status = 200, body = WebChatConversationResponse), + (status = 404, description = "Agent not found"), + (status = 500, description = "Internal server error"), + ), + tag = "webchat", +)] +pub(super) async fn create_webchat_conversation( + State(state): State>, + Json(request): Json, +) -> Result, StatusCode> { + let store = conversation_store(&state, &request.agent_id)?; + let conversation = store + .create(&request.agent_id, request.title.as_deref()) + .await + .map_err(|error| { + tracing::warn!(%error, agent_id = %request.agent_id, "failed to create webchat conversation"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + Ok(Json(WebChatConversationResponse { conversation })) +} + +#[utoipa::path( + put, + path = "/webchat/conversations/{session_id}", + request_body = UpdateWebChatConversationRequest, + params( + ("session_id" = String, Path, description = "Conversation session ID"), + ), + responses( + (status = 200, body = WebChatConversationResponse), + (status = 404, description = "Conversation not found"), + (status = 500, description = "Internal server error"), + ), + tag = "webchat", +)] +pub(super) async fn update_webchat_conversation( + State(state): State>, + Path(session_id): Path, + Json(request): Json, +) -> Result, StatusCode> { + let store = conversation_store(&state, &request.agent_id)?; + let conversation = store + .update( + &request.agent_id, + &session_id, + request.title.as_deref(), + request.archived, + ) + .await + .map_err(|error| { + tracing::warn!(%error, %session_id, "failed to update webchat conversation"); + StatusCode::INTERNAL_SERVER_ERROR + })? + .ok_or(StatusCode::NOT_FOUND)?; + + Ok(Json(WebChatConversationResponse { conversation })) +} + +#[utoipa::path( + delete, + path = "/webchat/conversations/{session_id}", + params( + ("session_id" = String, Path, description = "Conversation session ID"), + ("agent_id" = String, Query, description = "Agent ID"), + ), + responses( + (status = 200, body = WebChatSendResponse), + (status = 404, description = "Conversation not found"), + (status = 500, description = "Internal server error"), + ), + tag = "webchat", +)] +pub(super) async fn delete_webchat_conversation( + State(state): State>, + Path(session_id): Path, + Query(query): Query, +) -> Result, StatusCode> { + let store = conversation_store(&state, &query.agent_id)?; + let deleted = store + .delete(&query.agent_id, &session_id) + .await + .map_err(|error| { + tracing::warn!(%error, %session_id, "failed to delete webchat conversation"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + if !deleted { + return Err(StatusCode::NOT_FOUND); + } + + Ok(Json(WebChatSendResponse { ok: true })) +} diff --git a/src/conversation.rs b/src/conversation.rs index bbfc5fac9..3a3822ba5 100644 --- a/src/conversation.rs +++ b/src/conversation.rs @@ -3,10 +3,12 @@ pub mod channels; pub mod context; pub mod history; +pub mod webchat; pub mod worker_transcript; pub use channels::ChannelStore; pub use history::{ ConversationLogger, ProcessRunLogger, TimelineItem, WorkerDetailRow, WorkerRunRow, }; +pub use webchat::{WebChatConversation, WebChatConversationStore, WebChatConversationSummary}; pub use worker_transcript::{ActionContent, TranscriptStep}; diff --git a/src/conversation/webchat.rs b/src/conversation/webchat.rs new file mode 100644 index 000000000..f77f65df0 --- /dev/null +++ b/src/conversation/webchat.rs @@ -0,0 +1,347 @@ +//! Webchat conversation persistence (SQLite). + +use sqlx::{Row as _, SqlitePool}; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)] +pub struct WebChatConversation { + pub id: String, + pub agent_id: String, + pub title: String, + pub title_source: String, + pub archived: bool, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)] +pub struct WebChatConversationSummary { + pub id: String, + pub agent_id: String, + pub title: String, + pub title_source: String, + pub archived: bool, + pub created_at: chrono::DateTime, + pub updated_at: chrono::DateTime, + pub last_message_at: Option>, + pub last_message_preview: Option, + pub last_message_role: Option, + pub message_count: i64, +} + +#[derive(Debug, Clone)] +pub struct WebChatConversationStore { + pool: SqlitePool, +} + +impl WebChatConversationStore { + pub fn new(pool: SqlitePool) -> Self { + Self { pool } + } + + pub async fn create( + &self, + agent_id: &str, + title: Option<&str>, + ) -> crate::error::Result { + let id = format!("portal:chat:{agent_id}:{}", uuid::Uuid::new_v4()); + let title = normalize_title(title).unwrap_or_else(default_title); + let title_source = if title == default_title() { + "system" + } else { + "user" + }; + + sqlx::query( + "INSERT INTO webchat_conversations (id, agent_id, title, title_source) VALUES (?, ?, ?, ?)", + ) + .bind(&id) + .bind(agent_id) + .bind(&title) + .bind(title_source) + .execute(&self.pool) + .await + .map_err(|error| anyhow::anyhow!(error))?; + + Ok(self + .get(agent_id, &id) + .await? + .ok_or_else(|| anyhow::anyhow!("newly created webchat conversation missing"))?) + } + + pub async fn ensure( + &self, + agent_id: &str, + session_id: &str, + ) -> crate::error::Result { + sqlx::query( + "INSERT INTO webchat_conversations (id, agent_id, title, title_source) VALUES (?, ?, ?, 'system') \ + ON CONFLICT(id) DO NOTHING", + ) + .bind(session_id) + .bind(agent_id) + .bind(default_title()) + .execute(&self.pool) + .await + .map_err(|error| anyhow::anyhow!(error))?; + + Ok(self + .get(agent_id, session_id) + .await? + .ok_or_else(|| anyhow::anyhow!("ensured webchat conversation missing"))?) + } + + pub async fn get( + &self, + agent_id: &str, + session_id: &str, + ) -> crate::error::Result> { + let row = sqlx::query( + "SELECT id, agent_id, title, title_source, archived, created_at, updated_at \ + FROM webchat_conversations WHERE agent_id = ? AND id = ?", + ) + .bind(agent_id) + .bind(session_id) + .fetch_optional(&self.pool) + .await + .map_err(|error| anyhow::anyhow!(error))?; + + Ok(row.map(row_to_conversation)) + } + + pub async fn list( + &self, + agent_id: &str, + include_archived: bool, + limit: i64, + ) -> crate::error::Result> { + self.backfill_from_messages(agent_id).await?; + + let rows = sqlx::query( + "SELECT \ + c.id, c.agent_id, c.title, c.title_source, c.archived, c.created_at, c.updated_at, \ + (SELECT MAX(created_at) FROM conversation_messages WHERE channel_id = c.id) as last_message_at, \ + (SELECT content FROM conversation_messages WHERE channel_id = c.id ORDER BY created_at DESC LIMIT 1) as last_message_preview, \ + (SELECT role FROM conversation_messages WHERE channel_id = c.id ORDER BY created_at DESC LIMIT 1) as last_message_role, \ + (SELECT COUNT(*) FROM conversation_messages WHERE channel_id = c.id) as message_count \ + FROM webchat_conversations c \ + WHERE c.agent_id = ? AND (? = 1 OR c.archived = 0) \ + ORDER BY COALESCE((SELECT MAX(created_at) FROM conversation_messages WHERE channel_id = c.id), c.updated_at, c.created_at) DESC \ + LIMIT ?", + ) + .bind(agent_id) + .bind(if include_archived { 1_i64 } else { 0_i64 }) + .bind(limit.clamp(1, 500)) + .fetch_all(&self.pool) + .await + .map_err(|error| anyhow::anyhow!(error))?; + + Ok(rows.into_iter().map(row_to_summary).collect()) + } + + pub async fn update( + &self, + agent_id: &str, + session_id: &str, + title: Option<&str>, + archived: Option, + ) -> crate::error::Result> { + if title.is_none() && archived.is_none() { + return self.get(agent_id, session_id).await; + } + + let title = normalize_title(title); + let title_source = title.as_ref().map(|_| "user"); + + let result = sqlx::query( + "UPDATE webchat_conversations \ + SET title = COALESCE(?, title), \ + title_source = COALESCE(?, title_source), \ + archived = COALESCE(?, archived), \ + updated_at = CURRENT_TIMESTAMP \ + WHERE agent_id = ? AND id = ?", + ) + .bind(title.as_deref()) + .bind(title_source) + .bind(archived.map(|value| if value { 1_i64 } else { 0_i64 })) + .bind(agent_id) + .bind(session_id) + .execute(&self.pool) + .await + .map_err(|error| anyhow::anyhow!(error))?; + + if result.rows_affected() == 0 { + return Ok(None); + } + + self.get(agent_id, session_id).await + } + + pub async fn delete(&self, agent_id: &str, session_id: &str) -> crate::error::Result { + let mut tx = self + .pool + .begin() + .await + .map_err(|error| anyhow::anyhow!(error))?; + + sqlx::query("DELETE FROM conversation_messages WHERE channel_id = ?") + .bind(session_id) + .execute(&mut *tx) + .await + .map_err(|error| anyhow::anyhow!(error))?; + + let result = sqlx::query("DELETE FROM webchat_conversations WHERE agent_id = ? AND id = ?") + .bind(agent_id) + .bind(session_id) + .execute(&mut *tx) + .await + .map_err(|error| anyhow::anyhow!(error))?; + + tx.commit().await.map_err(|error| anyhow::anyhow!(error))?; + + Ok(result.rows_affected() > 0) + } + + pub async fn maybe_set_generated_title( + &self, + agent_id: &str, + session_id: &str, + content: &str, + ) -> crate::error::Result<()> { + let generated_title = generate_title(content); + + sqlx::query( + "UPDATE webchat_conversations \ + SET title = ?, updated_at = CURRENT_TIMESTAMP \ + WHERE agent_id = ? AND id = ? AND title_source = 'system' AND title = ?", + ) + .bind(&generated_title) + .bind(agent_id) + .bind(session_id) + .bind(default_title()) + .execute(&self.pool) + .await + .map_err(|error| anyhow::anyhow!(error))?; + + Ok(()) + } + + async fn backfill_from_messages(&self, agent_id: &str) -> crate::error::Result<()> { + let rows = sqlx::query( + "SELECT DISTINCT channel_id FROM conversation_messages WHERE channel_id LIKE 'portal:chat:%'", + ) + .fetch_all(&self.pool) + .await + .map_err(|error| anyhow::anyhow!(error))?; + + for row in rows { + let channel_id: String = row.try_get("channel_id").unwrap_or_default(); + if channel_id.is_empty() { + continue; + } + + let existing = self.get(agent_id, &channel_id).await?; + if existing.is_some() { + continue; + } + + let title = sqlx::query( + "SELECT content FROM conversation_messages \ + WHERE channel_id = ? AND role = 'user' \ + ORDER BY created_at ASC LIMIT 1", + ) + .bind(&channel_id) + .fetch_optional(&self.pool) + .await + .map_err(|error| anyhow::anyhow!(error))? + .and_then(|title_row| title_row.try_get::("content").ok()) + .map(|content| generate_title(&content)) + .unwrap_or_else(default_title); + + let title_source = if title == default_title() { + "system" + } else { + "user" + }; + + sqlx::query( + "INSERT INTO webchat_conversations (id, agent_id, title, title_source) VALUES (?, ?, ?, ?) \ + ON CONFLICT(id) DO NOTHING", + ) + .bind(&channel_id) + .bind(agent_id) + .bind(&title) + .bind(title_source) + .execute(&self.pool) + .await + .map_err(|error| anyhow::anyhow!(error))?; + } + + Ok(()) + } +} + +fn row_to_conversation(row: sqlx::sqlite::SqliteRow) -> WebChatConversation { + WebChatConversation { + id: row.try_get("id").unwrap_or_default(), + agent_id: row.try_get("agent_id").unwrap_or_default(), + title: row.try_get("title").unwrap_or_else(|_| default_title()), + title_source: row + .try_get("title_source") + .unwrap_or_else(|_| "system".to_string()), + archived: row.try_get::("archived").unwrap_or(0) == 1, + created_at: row + .try_get("created_at") + .unwrap_or_else(|_| chrono::Utc::now()), + updated_at: row + .try_get("updated_at") + .unwrap_or_else(|_| chrono::Utc::now()), + } +} + +fn row_to_summary(row: sqlx::sqlite::SqliteRow) -> WebChatConversationSummary { + WebChatConversationSummary { + id: row.try_get("id").unwrap_or_default(), + agent_id: row.try_get("agent_id").unwrap_or_default(), + title: row.try_get("title").unwrap_or_else(|_| default_title()), + title_source: row + .try_get("title_source") + .unwrap_or_else(|_| "system".to_string()), + archived: row.try_get::("archived").unwrap_or(0) == 1, + created_at: row + .try_get("created_at") + .unwrap_or_else(|_| chrono::Utc::now()), + updated_at: row + .try_get("updated_at") + .unwrap_or_else(|_| chrono::Utc::now()), + last_message_at: row.try_get("last_message_at").ok(), + last_message_preview: row.try_get("last_message_preview").ok().flatten(), + last_message_role: row.try_get("last_message_role").ok().flatten(), + message_count: row.try_get("message_count").unwrap_or(0), + } +} + +fn default_title() -> String { + "New chat".to_string() +} + +fn normalize_title(title: Option<&str>) -> Option { + title + .map(str::trim) + .filter(|title| !title.is_empty()) + .map(ToString::to_string) +} + +fn generate_title(content: &str) -> String { + let cleaned = content.trim().replace('\n', " "); + let trimmed = cleaned.trim(); + + if trimmed.is_empty() { + return default_title(); + } + + let mut title = trimmed.chars().take(72).collect::(); + if trimmed.chars().count() > 72 { + title.push_str("..."); + } + title +} diff --git a/src/hooks/spacebot.rs b/src/hooks/spacebot.rs index 72bde376a..9fbe084d3 100644 --- a/src/hooks/spacebot.rs +++ b/src/hooks/spacebot.rs @@ -3,8 +3,13 @@ use crate::hooks::loop_guard::{LoopGuard, LoopGuardConfig, LoopGuardVerdict}; use crate::tools::{MemoryPersistenceContractState, MemoryPersistenceTerminalOutcome}; use crate::{AgentId, ChannelId, ProcessEvent, ProcessId, ProcessType}; +use futures::StreamExt; use rig::agent::{HookAction, PromptHook, ToolCallHookAction}; -use rig::completion::{CompletionModel, CompletionResponse, Message, Prompt, PromptError}; +use rig::completion::{ + CompletionModel, CompletionResponse, GetTokenUsage, Message, Prompt, PromptError, +}; +use rig::message::{AssistantContent, ToolResultContent, UserContent}; +use rig::streaming::{StreamedAssistantContent, StreamingCompletion}; use std::sync::Arc; use tokio::sync::broadcast; @@ -61,6 +66,15 @@ pub struct SpacebotHook { /// append the messages to history before re-prompting. injected_messages: std::sync::Arc>>, memory_persistence_contract: Option>, + reply_tool_delta_state: + std::sync::Arc>>, +} + +#[derive(Clone, Debug, Default)] +struct ReplyToolDeltaState { + tool_name: Option, + raw_args: String, + emitted_content: String, } impl SpacebotHook { @@ -115,6 +129,9 @@ impl SpacebotHook { inject_rx: None, injected_messages: std::sync::Arc::new(std::sync::Mutex::new(Vec::new())), memory_persistence_contract: None, + reply_tool_delta_state: std::sync::Arc::new(std::sync::Mutex::new( + std::collections::HashMap::new(), + )), } } @@ -176,6 +193,51 @@ impl SpacebotHook { .store(active, std::sync::atomic::Ordering::Relaxed); } + fn extract_partial_reply_content(raw_args: &str) -> Option { + let key_index = raw_args.find("\"content\"")?; + let after_key = &raw_args[key_index + "\"content\"".len()..]; + let colon_index = after_key.find(':')?; + let after_colon = &after_key[colon_index + 1..]; + let quote_index = after_colon.find('"')?; + let content_slice = &after_colon[quote_index + 1..]; + + let mut result = String::new(); + let mut chars = content_slice.chars(); + while let Some(ch) = chars.next() { + match ch { + '"' => break, + '\\' => { + let Some(escaped) = chars.next() else { + break; + }; + match escaped { + '"' => result.push('"'), + '\\' => result.push('\\'), + '/' => result.push('/'), + 'b' => result.push('\u{0008}'), + 'f' => result.push('\u{000C}'), + 'n' => result.push('\n'), + 'r' => result.push('\r'), + 't' => result.push('\t'), + 'u' => { + let hex: String = chars.by_ref().take(4).collect(); + if hex.len() == 4 + && let Ok(value) = u32::from_str_radix(&hex, 16) + && let Some(decoded) = char::from_u32(value) + { + result.push(decoded); + } + } + other => result.push(other), + } + } + other => result.push(other), + } + } + + Some(result) + } + /// Return true if a PromptCancelled reason indicates a tool nudge retry. pub fn is_tool_nudge_reason(reason: &str) -> bool { reason == Self::TOOL_NUDGE_REASON @@ -380,6 +442,260 @@ impl SpacebotHook { .await } + /// Prompt once using Rig's streaming path so text/tool deltas reach the hook. + pub async fn prompt_once_streaming( + &self, + agent: &rig::agent::Agent, + history: &mut Vec, + prompt: &str, + max_turns: usize, + ) -> std::result::Result + where + M: CompletionModel + 'static, + M::StreamingResponse: GetTokenUsage + Send, + { + self.reset_tool_nudge_state(); + self.set_tool_nudge_request_active(false); + + let mut chat_history = history.clone(); + let prompt_message = Message::from(prompt); + chat_history.push(prompt_message.clone()); + + let mut current_max_turns = 0usize; + let mut last_text_response = String::new(); + let mut did_call_tool = false; + + loop { + let current_prompt = chat_history + .last() + .cloned() + .expect("chat history should always include current prompt"); + + if current_max_turns > max_turns + 1 { + return Err(PromptError::MaxTurnsError { + max_turns, + chat_history: Box::new(chat_history), + prompt: Box::new(prompt.to_string().into()), + }); + } + + current_max_turns += 1; + + if let HookAction::Terminate { reason } = + >::on_completion_call( + self, + ¤t_prompt, + &chat_history[..chat_history.len() - 1], + ) + .await + { + return Err(PromptError::PromptCancelled { + chat_history: Box::new(chat_history), + reason, + }); + } + + let request = agent + .stream_completion( + current_prompt.clone(), + chat_history[..chat_history.len() - 1].to_vec(), + ) + .await + .map_err(PromptError::CompletionError)?; + + let mut stream = request + .stream() + .await + .map_err(PromptError::CompletionError)?; + + let mut tool_calls = vec![]; + let mut tool_results = vec![]; + let mut is_text_response = false; + + while let Some(content) = stream.next().await { + match content.map_err(PromptError::CompletionError)? { + StreamedAssistantContent::Text(text) => { + if !is_text_response { + last_text_response.clear(); + is_text_response = true; + } + last_text_response.push_str(&text.text); + if let HookAction::Terminate { reason } = + >::on_text_delta( + self, + &text.text, + &last_text_response, + ) + .await + { + return Err(PromptError::PromptCancelled { + chat_history: Box::new(chat_history), + reason, + }); + } + did_call_tool = false; + } + StreamedAssistantContent::ToolCall { + tool_call, + internal_call_id, + } => { + let tool_args = serde_json::to_string(&tool_call.function.arguments) + .unwrap_or_else(|_| "{}".to_string()); + match >::on_tool_call( + self, + &tool_call.function.name, + tool_call.call_id.clone(), + &internal_call_id, + &tool_args, + ) + .await + { + ToolCallHookAction::Terminate { reason } => { + return Err(PromptError::PromptCancelled { + chat_history: Box::new(chat_history), + reason, + }); + } + ToolCallHookAction::Skip { reason } => { + tool_calls.push(AssistantContent::ToolCall(tool_call.clone())); + tool_results.push(( + tool_call.id.clone(), + tool_call.call_id.clone(), + reason, + )); + did_call_tool = true; + } + ToolCallHookAction::Continue => { + let tool_result = match agent + .tool_server_handle + .call_tool(&tool_call.function.name, &tool_args) + .await + { + Ok(result) => result, + Err(error) => error.to_string(), + }; + + if let HookAction::Terminate { reason } = + >::on_tool_result( + self, + &tool_call.function.name, + tool_call.call_id.clone(), + &internal_call_id, + &tool_args, + &tool_result, + ) + .await + { + return Err(PromptError::PromptCancelled { + chat_history: Box::new(chat_history), + reason, + }); + } + + tool_calls.push(AssistantContent::ToolCall(tool_call.clone())); + tool_results.push(( + tool_call.id.clone(), + tool_call.call_id.clone(), + tool_result, + )); + did_call_tool = true; + } + } + } + StreamedAssistantContent::ToolCallDelta { + id, + internal_call_id, + content, + } => { + let (name, delta) = match &content { + rig::streaming::ToolCallDeltaContent::Name(name) => { + (Some(name.as_str()), "") + } + rig::streaming::ToolCallDeltaContent::Delta(delta) => { + (None, delta.as_str()) + } + }; + if let HookAction::Terminate { reason } = + >::on_tool_call_delta( + self, + &id, + &internal_call_id, + name, + delta, + ) + .await + { + return Err(PromptError::PromptCancelled { + chat_history: Box::new(chat_history), + reason, + }); + } + } + StreamedAssistantContent::Final(final_response) => { + if is_text_response { + if let HookAction::Terminate { reason } = + >::on_stream_completion_response_finish( + self, + ¤t_prompt, + &final_response, + ) + .await + { + return Err(PromptError::PromptCancelled { + chat_history: Box::new(chat_history), + reason, + }); + } + is_text_response = false; + } + } + StreamedAssistantContent::Reasoning(_) + | StreamedAssistantContent::ReasoningDelta { .. } => { + did_call_tool = false; + } + } + } + + if !tool_calls.is_empty() { + chat_history.push(Message::Assistant { + id: None, + content: rig::OneOrMany::many(tool_calls) + .expect("tool call list should not be empty"), + }); + } + + for (id, call_id, tool_result) in tool_results { + if let Some(call_id) = call_id { + chat_history.push(Message::User { + content: rig::OneOrMany::one(UserContent::tool_result_with_call_id( + &id, + call_id, + rig::OneOrMany::one(ToolResultContent::text(&tool_result)), + )), + }); + } else { + chat_history.push(Message::User { + content: rig::OneOrMany::one(UserContent::tool_result( + &id, + rig::OneOrMany::one(ToolResultContent::text(&tool_result)), + )), + }); + } + } + + if !did_call_tool { + chat_history.push(Message::Assistant { + id: None, + content: rig::OneOrMany::one(AssistantContent::text( + last_text_response.clone(), + )), + }); + *history = chat_history; + return Ok(last_text_response); + } + } + } + /// Send a status update event. pub fn send_status(&self, status: impl Into) { let event = ProcessEvent::StatusUpdate { @@ -778,6 +1094,74 @@ where HookAction::Continue } + async fn on_tool_call_delta( + &self, + _tool_call_id: &str, + internal_call_id: &str, + tool_name: Option<&str>, + tool_call_delta: &str, + ) -> HookAction { + if self.process_type != ProcessType::Channel { + return HookAction::Continue; + } + + let Some(channel_id) = self.channel_id.clone() else { + return HookAction::Continue; + }; + + let mut guard = match self.reply_tool_delta_state.lock() { + Ok(guard) => guard, + Err(_) => return HookAction::Continue, + }; + + let state = guard + .entry(internal_call_id.to_string()) + .or_insert_with(ReplyToolDeltaState::default); + + if let Some(tool_name) = tool_name { + state.tool_name = Some(tool_name.to_string()); + } + + if state.tool_name.as_deref() != Some("reply") { + return HookAction::Continue; + } + + state.raw_args.push_str(tool_call_delta); + let Some(content) = Self::extract_partial_reply_content(&state.raw_args) else { + return HookAction::Continue; + }; + + if !content.starts_with(&state.emitted_content) { + return HookAction::Continue; + } + + let delta = &content[state.emitted_content.len()..]; + if delta.is_empty() { + return HookAction::Continue; + } + + state.emitted_content = content.clone(); + self.event_tx + .send(ProcessEvent::TextDelta { + agent_id: self.agent_id.clone(), + process_id: self.process_id.clone(), + channel_id: Some(channel_id), + text_delta: delta.to_string(), + aggregated_text: content, + }) + .ok(); + + HookAction::Continue + } + + async fn on_stream_completion_response_finish( + &self, + _prompt: &Message, + _response: &::StreamingResponse, + ) -> HookAction { + HookAction::Continue + } + async fn on_tool_call( &self, tool_name: &str, @@ -785,6 +1169,11 @@ where _internal_call_id: &str, args: &str, ) -> ToolCallHookAction { + if tool_name == "reply" + && let Ok(mut guard) = self.reply_tool_delta_state.lock() + { + guard.remove(_internal_call_id); + } // Loop guard: check for repetitive tool calling before execution. // Runs for all process types. Block → Skip (message becomes tool // result), CircuitBreak → Terminate. @@ -860,6 +1249,11 @@ where _args: &str, result: &str, ) -> HookAction { + if tool_name == "reply" + && let Ok(mut guard) = self.reply_tool_delta_state.lock() + { + guard.remove(internal_call_id); + } let guard_action = self.guard_tool_result(tool_name, result); if !matches!(guard_action, HookAction::Continue) { self.record_tool_result_metrics(tool_name, internal_call_id); @@ -1524,6 +1918,58 @@ mod tests { )); } + #[tokio::test] + async fn reply_tool_call_delta_emits_process_event() { + let (event_tx, mut event_rx) = tokio::sync::broadcast::channel(8); + let hook = SpacebotHook::new( + std::sync::Arc::::from("agent"), + ProcessId::Channel(std::sync::Arc::::from("channel")), + ProcessType::Channel, + Some(std::sync::Arc::::from("channel")), + event_tx, + ); + + let first = >::on_tool_call_delta( + &hook, + "reply-call", + "internal-reply", + Some("reply"), + "{\"content\":\"hel", + ) + .await; + let second = >::on_tool_call_delta( + &hook, + "reply-call", + "internal-reply", + None, + "lo\"}", + ) + .await; + + assert!(matches!(first, HookAction::Continue)); + assert!(matches!(second, HookAction::Continue)); + + let event = event_rx.recv().await.expect("first reply delta event"); + assert!(matches!( + event, + ProcessEvent::TextDelta { + ref text_delta, + ref aggregated_text, + .. + } if text_delta == "hel" && aggregated_text == "hel" + )); + + let event = event_rx.recv().await.expect("second reply delta event"); + assert!(matches!( + event, + ProcessEvent::TextDelta { + ref text_delta, + ref aggregated_text, + .. + } if text_delta == "lo" && aggregated_text == "hello" + )); + } + #[tokio::test] async fn tool_result_resets_consecutive_nudge_counter() { // The exact scenario from the Railway browser worker failure: diff --git a/src/llm/model.rs b/src/llm/model.rs index ddaef3cd2..72879d89c 100644 --- a/src/llm/model.rs +++ b/src/llm/model.rs @@ -13,6 +13,13 @@ use rig::message::{ ToolCall, ToolFunction, UserContent, }; use rig::one_or_many::OneOrMany; +use rig::providers::openai::responses_api::Output as OpenAiResponsesOutput; +use rig::providers::openai::responses_api::streaming::{ + ItemChunkKind as OpenAiResponsesItemChunkKind, + ResponseChunkKind as OpenAiResponsesResponseChunkKind, + StreamingCompletionChunk as OpenAiResponsesStreamingCompletionChunk, + StreamingItemDoneOutput as OpenAiResponsesStreamingItemDoneOutput, +}; use rig::streaming::{RawStreamingChoice, RawStreamingToolCall, StreamingCompletionResponse}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; @@ -541,10 +548,14 @@ impl CompletionModel for SpacebotModel { self.stream_openai_compatible(request, "Google Gemini", &provider_config) .await } - ApiType::Anthropic | ApiType::OpenAiResponses => { + ApiType::Anthropic => { let response = self.attempt_completion(request).await?; Ok(stream_from_completion_response(response)) } + ApiType::OpenAiResponses => { + self.stream_openai_responses(request, &provider_config) + .await + } } } } @@ -848,6 +859,287 @@ impl SpacebotModel { parse_openai_responses_response(response_body, &provider_label) } + async fn stream_openai_responses( + &self, + request: CompletionRequest, + provider_config: &ProviderConfig, + ) -> Result, CompletionError> { + let base_url = provider_config.base_url.trim_end_matches('/'); + let is_chatgpt_codex = self.provider == "openai-chatgpt"; + let responses_url = if is_chatgpt_codex { + format!("{base_url}/responses") + } else { + format!("{base_url}/v1/responses") + }; + let api_key = provider_config.api_key.as_str(); + let provider_label = provider_config + .name + .as_deref() + .filter(|name| !name.trim().is_empty()) + .map(ToOwned::to_owned) + .unwrap_or_else(|| provider_display_name(&self.provider)); + + let input = convert_messages_to_openai_responses(&request.chat_history); + let api_model_name = self.remap_model_name_for_api(); + let mut body = serde_json::json!({ + "model": api_model_name, + "input": input, + "stream": true, + }); + + if let Some(preamble) = &request.preamble { + body["instructions"] = serde_json::json!(preamble); + } else if is_chatgpt_codex { + body["instructions"] = serde_json::json!( + "You are Spacebot. Follow instructions exactly and respond concisely." + ); + } + + if !is_chatgpt_codex && let Some(max_tokens) = request.max_tokens { + body["max_output_tokens"] = serde_json::json!(max_tokens); + } + + if !is_chatgpt_codex && let Some(temperature) = request.temperature { + body["temperature"] = serde_json::json!(temperature); + } + + if is_chatgpt_codex { + body["store"] = serde_json::json!(false); + } + + if !request.tools.is_empty() { + let tools: Vec = request + .tools + .iter() + .map(|tool_definition| { + serde_json::json!({ + "type": "function", + "name": tool_definition.name, + "description": tool_definition.description, + "parameters": tool_definition.parameters, + }) + }) + .collect(); + body["tools"] = serde_json::json!(tools); + } + + let openai_account_id = if self.provider == "openai-chatgpt" { + self.llm_manager.get_openai_account_id().await + } else { + None + }; + + let mut request_builder = self + .llm_manager + .http_client() + .post(&responses_url) + .header("authorization", format!("Bearer {api_key}")) + .header("content-type", "application/json") + .header("accept-encoding", "identity") + .timeout(std::time::Duration::from_secs(STREAM_REQUEST_TIMEOUT_SECS)); + if let Some(account_id) = openai_account_id { + request_builder = request_builder.header("ChatGPT-Account-Id", account_id); + } + if is_chatgpt_codex { + request_builder = request_builder + .header("originator", "opencode") + .header( + "session_id", + format!("spacebot-{}", chrono::Utc::now().timestamp()), + ) + .header( + "user-agent", + format!("spacebot/{}", env!("CARGO_PKG_VERSION")), + ); + } + + let response = request_builder + .json(&body) + .send() + .await + .map_err(|error| CompletionError::ProviderError(error.to_string()))?; + + let status = response.status(); + if !status.is_success() { + let response_text = response + .text() + .await + .unwrap_or_else(|error| format!("failed to read error response body: {error}")); + + return Err(CompletionError::ProviderError(format!( + "{provider_label} Responses API error ({status}): {}", + parse_openai_error_message(&response_text) + .unwrap_or_else(|| "unknown error".to_string()) + ))); + } + + let provider_label = provider_label.to_string(); + let stream = async_stream::stream! { + let mut stream = response.bytes_stream(); + let mut block_buffer = String::new(); + let mut raw_text = String::new(); + let mut sse_text = String::new(); + let mut saw_data_event = false; + let mut pending_tool_calls: std::collections::HashMap = std::collections::HashMap::new(); + + while let Some(chunk_result) = stream.next().await { + let chunk = match chunk_result { + Ok(bytes) => bytes, + Err(error) => { + yield Err(CompletionError::ProviderError(format!( + "{provider_label} stream read failed: {error}" + ))); + return; + } + }; + + let chunk_text = String::from_utf8_lossy(&chunk).to_string(); + if !saw_data_event { + raw_text.push_str(&chunk_text); + } + block_buffer.push_str(&chunk_text); + + while let Some(block) = extract_sse_block(&mut block_buffer) { + sse_text.push_str(&block); + sse_text.push_str("\n\n"); + + let Some(data) = extract_sse_data_payload(&block) else { + continue; + }; + let data = data.trim(); + if data.is_empty() || data == "[DONE]" { + continue; + } + + saw_data_event = true; + let event = match serde_json::from_str::(data) { + Ok(event) => event, + Err(_) => { + let raw_event = match serde_json::from_str::(data) { + Ok(raw_event) => raw_event, + Err(_) => continue, + }; + match process_openai_responses_stream_raw_event(&raw_event, &mut pending_tool_calls) { + Ok(events) => { + for event in events { + yield Ok(event); + } + } + Err(error) => { + yield Err(error); + return; + } + } + continue; + } + }; + + match process_openai_responses_stream_event(&event, &mut pending_tool_calls) { + Ok(events) => { + for event in events { + yield Ok(event); + } + } + Err(error) => { + yield Err(error); + return; + } + } + } + } + + if !block_buffer.trim().is_empty() && let Some(data) = extract_sse_data_payload(&block_buffer) { + let data = data.trim(); + if !data.is_empty() && data != "[DONE]" { + saw_data_event = true; + if let Ok(event) = serde_json::from_str::(data) { + match process_openai_responses_stream_event(&event, &mut pending_tool_calls) { + Ok(events) => { + for event in events { + yield Ok(event); + } + } + Err(error) => { + yield Err(error); + return; + } + } + } else { + if let Ok(raw_event) = serde_json::from_str::(data) { + match process_openai_responses_stream_raw_event(&raw_event, &mut pending_tool_calls) { + Ok(events) => { + for event in events { + yield Ok(event); + } + } + Err(error) => { + yield Err(error); + return; + } + } + } + } + } + } + + if saw_data_event { + let response_body = match parse_openai_responses_sse_response(&sse_text, &provider_label) { + Ok(body) => body, + Err(error) => { + yield Err(error); + return; + } + }; + + let parsed_response = match parse_openai_responses_response(response_body.clone(), &provider_label) { + Ok(response) => response, + Err(error) => { + yield Err(error); + return; + } + }; + + yield Ok(RawStreamingChoice::FinalResponse(RawStreamingResponse { + body: response_body, + usage: Some(parsed_response.usage), + })); + return; + } + + let response_body = match serde_json::from_str::(&raw_text) { + Ok(body) => body, + Err(error) => { + yield Err(CompletionError::ProviderError(format!( + "{provider_label} response is neither SSE nor JSON: {error}. Body: {}", + truncate_body(&raw_text) + ))); + return; + } + }; + + let parsed_response = match parse_openai_responses_response(response_body.clone(), &provider_label) { + Ok(response) => response, + Err(error) => { + yield Err(error); + return; + } + }; + + for event in completion_choice_to_streaming_choices(&parsed_response.choice) { + yield Ok(event); + } + if let Some(message_id) = parsed_response.message_id { + yield Ok(RawStreamingChoice::MessageId(message_id)); + } + yield Ok(RawStreamingChoice::FinalResponse(RawStreamingResponse { + body: response_body, + usage: Some(parsed_response.usage), + })); + }; + + Ok(StreamingCompletionResponse::stream(Box::pin(stream))) + } + /// Generic OpenAI-compatible API call. /// Used by providers that implement the OpenAI chat completions format. #[allow(dead_code)] @@ -2117,6 +2409,268 @@ fn process_openai_chat_stream_event( Ok(events) } +fn process_openai_responses_stream_event( + event: &OpenAiResponsesStreamingCompletionChunk, + pending_tool_calls: &mut std::collections::HashMap, +) -> Result>, CompletionError> { + let mut events = Vec::new(); + + match event { + OpenAiResponsesStreamingCompletionChunk::Delta(chunk) => { + match &chunk.data { + OpenAiResponsesItemChunkKind::OutputItemAdded(message) => { + if let OpenAiResponsesStreamingItemDoneOutput { + item: OpenAiResponsesOutput::FunctionCall(function_call), + .. + } = message + { + let entry = pending_tool_calls + .entry(function_call.id.clone()) + .or_insert_with(OpenAiStreamingToolCall::default); + entry.id = function_call.id.clone(); + entry.name = function_call.name.clone(); + events.push(RawStreamingChoice::ToolCallDelta { + id: function_call.id.clone(), + internal_call_id: entry.internal_call_id.clone(), + content: rig::streaming::ToolCallDeltaContent::Name( + function_call.name.clone(), + ), + }); + } + } + OpenAiResponsesItemChunkKind::OutputItemDone(message) => match message { + OpenAiResponsesStreamingItemDoneOutput { + item: OpenAiResponsesOutput::FunctionCall(function_call), + .. + } => { + let entry = pending_tool_calls + .remove(&function_call.id) + .unwrap_or_default(); + events.push(RawStreamingChoice::ToolCall(RawStreamingToolCall { + id: function_call.id.clone(), + internal_call_id: entry.internal_call_id, + call_id: Some(function_call.call_id.clone()), + name: function_call.name.clone(), + arguments: function_call.arguments.clone(), + signature: None, + additional_params: None, + })); + } + OpenAiResponsesStreamingItemDoneOutput { + item: OpenAiResponsesOutput::Message(message), + .. + } => { + events.push(RawStreamingChoice::MessageId(message.id.clone())); + } + OpenAiResponsesStreamingItemDoneOutput { + item: + OpenAiResponsesOutput::Reasoning { + summary, + id, + encrypted_content, + .. + }, + .. + } => { + for reasoning_summary in summary { + let rig::providers::openai::responses_api::ReasoningSummary::SummaryText { text } = reasoning_summary; + events.push(RawStreamingChoice::Reasoning { + id: Some(id.clone()), + content: ReasoningContent::Summary(text.clone()), + }); + } + if let Some(encrypted_content) = encrypted_content { + events.push(RawStreamingChoice::Reasoning { + id: Some(id.clone()), + content: ReasoningContent::Encrypted(encrypted_content.clone()), + }); + } + } + }, + OpenAiResponsesItemChunkKind::OutputTextDelta(delta) + | OpenAiResponsesItemChunkKind::RefusalDelta(delta) => { + events.push(RawStreamingChoice::Message(delta.delta.clone())); + } + OpenAiResponsesItemChunkKind::ReasoningSummaryTextDelta(delta) => { + events.push(RawStreamingChoice::ReasoningDelta { + id: None, + reasoning: delta.delta.clone(), + }); + } + OpenAiResponsesItemChunkKind::FunctionCallArgsDelta(delta) => { + let entry = pending_tool_calls + .entry(delta.item_id.clone()) + .or_insert_with(OpenAiStreamingToolCall::default); + entry.id = delta.item_id.clone(); + entry.arguments.push_str(&delta.delta); + events.push(RawStreamingChoice::ToolCallDelta { + id: delta.item_id.clone(), + internal_call_id: entry.internal_call_id.clone(), + content: rig::streaming::ToolCallDeltaContent::Delta(delta.delta.clone()), + }); + } + _ => {} + } + } + OpenAiResponsesStreamingCompletionChunk::Response(chunk) => { + if !matches!( + chunk.kind, + OpenAiResponsesResponseChunkKind::ResponseCompleted + ) { + return Ok(events); + } + } + } + + Ok(events) +} + +fn process_openai_responses_stream_raw_event( + event: &serde_json::Value, + pending_tool_calls: &mut std::collections::HashMap, +) -> Result>, CompletionError> { + let mut events = Vec::new(); + let Some(kind) = event.get("type").and_then(serde_json::Value::as_str) else { + return Ok(events); + }; + + match kind { + "response.output_text.delta" | "response.refusal.delta" => { + if let Some(delta) = event.get("delta").and_then(serde_json::Value::as_str) + && !delta.is_empty() + { + events.push(RawStreamingChoice::Message(delta.to_string())); + } + } + "response.reasoning_summary_text.delta" => { + if let Some(delta) = event.get("delta").and_then(serde_json::Value::as_str) + && !delta.is_empty() + { + events.push(RawStreamingChoice::ReasoningDelta { + id: None, + reasoning: delta.to_string(), + }); + } + } + "response.output_item.added" => { + let Some(item) = event.get("item") else { + return Ok(events); + }; + if item.get("type").and_then(serde_json::Value::as_str) == Some("function_call") { + let id = item + .get("id") + .and_then(serde_json::Value::as_str) + .unwrap_or_default() + .to_string(); + let name = item + .get("name") + .and_then(serde_json::Value::as_str) + .unwrap_or_default() + .to_string(); + if !id.is_empty() && !name.is_empty() { + let entry = pending_tool_calls.entry(id.clone()).or_default(); + entry.id = id.clone(); + entry.name = name.clone(); + events.push(RawStreamingChoice::ToolCallDelta { + id, + internal_call_id: entry.internal_call_id.clone(), + content: rig::streaming::ToolCallDeltaContent::Name(name), + }); + } + } + } + "response.function_call_arguments.delta" => { + let Some(item_id) = event.get("item_id").and_then(serde_json::Value::as_str) else { + return Ok(events); + }; + let Some(delta) = event.get("delta").and_then(serde_json::Value::as_str) else { + return Ok(events); + }; + let entry = pending_tool_calls.entry(item_id.to_string()).or_default(); + entry.id = item_id.to_string(); + entry.arguments.push_str(delta); + events.push(RawStreamingChoice::ToolCallDelta { + id: item_id.to_string(), + internal_call_id: entry.internal_call_id.clone(), + content: rig::streaming::ToolCallDeltaContent::Delta(delta.to_string()), + }); + } + "response.function_call_arguments.done" => {} + "response.output_item.done" => { + let Some(item) = event.get("item") else { + return Ok(events); + }; + match item.get("type").and_then(serde_json::Value::as_str) { + Some("function_call") => { + let id = item + .get("id") + .and_then(serde_json::Value::as_str) + .unwrap_or_default() + .to_string(); + let name = item + .get("name") + .and_then(serde_json::Value::as_str) + .unwrap_or_default() + .to_string(); + let call_id = item + .get("call_id") + .and_then(serde_json::Value::as_str) + .map(ToOwned::to_owned); + let arguments = parse_openai_tool_arguments( + item.get("arguments").unwrap_or(&serde_json::Value::Null), + ); + let entry = pending_tool_calls.remove(&id).unwrap_or_default(); + events.push(RawStreamingChoice::ToolCall(RawStreamingToolCall { + id, + internal_call_id: entry.internal_call_id, + call_id, + name, + arguments, + signature: None, + additional_params: None, + })); + } + Some("message") => { + if let Some(id) = item.get("id").and_then(serde_json::Value::as_str) { + events.push(RawStreamingChoice::MessageId(id.to_string())); + } + } + Some("reasoning") => { + if let Some(id) = item.get("id").and_then(serde_json::Value::as_str) { + if let Some(summary) = + item.get("summary").and_then(serde_json::Value::as_array) + { + for part in summary { + if let Some(text) = + part.get("text").and_then(serde_json::Value::as_str) + { + events.push(RawStreamingChoice::Reasoning { + id: Some(id.to_string()), + content: ReasoningContent::Summary(text.to_string()), + }); + } + } + } + if let Some(encrypted) = item + .get("encrypted_content") + .and_then(serde_json::Value::as_str) + { + events.push(RawStreamingChoice::Reasoning { + id: Some(id.to_string()), + content: ReasoningContent::Encrypted(encrypted.to_string()), + }); + } + } + } + _ => {} + } + } + _ => {} + } + + Ok(events) +} + fn parse_openai_chat_sse_response( response_text: &str, provider_label: &str, @@ -3675,6 +4229,86 @@ mod tests { assert_eq!(tool_calls[0].arguments["content"], "line1\nline2"); } + #[test] + fn process_openai_responses_stream_event_emits_text_and_tool_call_deltas() { + let mut pending = std::collections::HashMap::new(); + + let text_event = OpenAiResponsesStreamingCompletionChunk::Delta( + rig::providers::openai::responses_api::streaming::ItemChunk { + item_id: Some("msg_1".to_string()), + output_index: 0, + data: OpenAiResponsesItemChunkKind::OutputTextDelta( + rig::providers::openai::responses_api::streaming::DeltaTextChunk { + content_index: 0, + sequence_number: 1, + delta: "hello".to_string(), + }, + ), + }, + ); + + let tool_name_event = OpenAiResponsesStreamingCompletionChunk::Delta( + rig::providers::openai::responses_api::streaming::ItemChunk { + item_id: Some("fc_1".to_string()), + output_index: 0, + data: OpenAiResponsesItemChunkKind::OutputItemAdded( + OpenAiResponsesStreamingItemDoneOutput { + sequence_number: 2, + item: OpenAiResponsesOutput::FunctionCall( + rig::providers::openai::responses_api::OutputFunctionCall { + id: "fc_1".to_string(), + arguments: serde_json::json!({}), + call_id: "call_1".to_string(), + name: "reply".to_string(), + status: + rig::providers::openai::responses_api::ToolStatus::InProgress, + }, + ), + }, + ), + }, + ); + + let tool_args_event = OpenAiResponsesStreamingCompletionChunk::Delta( + rig::providers::openai::responses_api::streaming::ItemChunk { + item_id: Some("fc_1".to_string()), + output_index: 0, + data: OpenAiResponsesItemChunkKind::FunctionCallArgsDelta( + rig::providers::openai::responses_api::streaming::DeltaTextChunkWithItemId { + item_id: "fc_1".to_string(), + content_index: 0, + sequence_number: 3, + delta: "{\"content\":\"hi\"}".to_string(), + }, + ), + }, + ); + + let text_events = process_openai_responses_stream_event(&text_event, &mut pending) + .expect("text event should parse"); + assert!(matches!( + text_events.first(), + Some(RawStreamingChoice::Message(text)) if text == "hello" + )); + + let tool_name_events = + process_openai_responses_stream_event(&tool_name_event, &mut pending) + .expect("tool name event should parse"); + assert!(matches!( + tool_name_events.first(), + Some(RawStreamingChoice::ToolCallDelta { id, content, .. }) + if id == "fc_1" && matches!(content, rig::streaming::ToolCallDeltaContent::Name(name) if name == "reply") + )); + + let tool_arg_events = process_openai_responses_stream_event(&tool_args_event, &mut pending) + .expect("tool args event should parse"); + assert!(matches!( + tool_arg_events.first(), + Some(RawStreamingChoice::ToolCallDelta { id, content, .. }) + if id == "fc_1" && matches!(content, rig::streaming::ToolCallDeltaContent::Delta(delta) if delta == "{\"content\":\"hi\"}") + )); + } + #[test] fn parse_openai_chat_sse_response_merges_multiline_data_blocks() { let sse = concat!(