diff --git a/package-lock.json b/package-lock.json index a109bbb..87a7bad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,16 @@ "version": "0.0.0", "license": "MIT OR Apache-2.0", "dependencies": { - "@tauri-apps/api": "^2.0.0" + "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.7.1", + "dompurify": "^3.4.11", + "marked": "^18.0.5" }, "devDependencies": { "@sveltejs/vite-plugin-svelte": "^4.0.0", "@tauri-apps/cli": "^2.0.0", "@tsconfig/svelte": "^5.0.0", + "@types/dompurify": "^3.0.5", "eslint": "^9.0.0", "eslint-plugin-svelte": "^3.20.0", "globals": "^17.7.0", @@ -1383,6 +1387,15 @@ "node": ">= 10" } }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz", + "integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, "node_modules/@tsconfig/svelte": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.8.tgz", @@ -1390,6 +1403,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/trusted-types": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -1408,7 +1431,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -1944,6 +1967,15 @@ "dev": true, "license": "MIT" }, + "node_modules/dompurify": { + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -2575,6 +2607,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", diff --git a/package.json b/package.json index ca00003..fe3dff0 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@sveltejs/vite-plugin-svelte": "^4.0.0", "@tauri-apps/cli": "^2.0.0", "@tsconfig/svelte": "^5.0.0", + "@types/dompurify": "^3.0.5", "eslint": "^9.0.0", "eslint-plugin-svelte": "^3.20.0", "globals": "^17.7.0", @@ -31,6 +32,9 @@ "vite": "^5.4.0" }, "dependencies": { - "@tauri-apps/api": "^2.0.0" + "@tauri-apps/api": "^2.0.0", + "@tauri-apps/plugin-dialog": "^2.7.1", + "dompurify": "^3.4.11", + "marked": "^18.0.5" } } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 6e6efba..5612ddb 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2408,6 +2408,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" dependencies = [ "bitflags 2.13.0", "block2", + "libc", "objc2", "objc2-core-foundation", ] @@ -3109,6 +3110,30 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + [[package]] name = "ring" version = "0.17.14" @@ -4153,6 +4178,64 @@ dependencies = [ "tauri-utils", ] +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + [[package]] name = "tauri-runtime" version = "2.11.3" @@ -5138,6 +5221,7 @@ dependencies = [ "sqlx", "tauri", "tauri-build", + "tauri-plugin-dialog", "thiserror 1.0.69", "tokio", "tracing", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index c704db4..b97974a 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -42,6 +42,7 @@ rmcp = { version = "0.16", optional = true, features = ["server"] } # MCP serve # feature-gated so the CPU-only build always compiles (NFR-MNT-4). hound = { version = "3", optional = true } # WAV I/O (Phase 1) whisper-rs = { version = "0.16", optional = true } # whisper.cpp bindings (Phase 1) +tauri-plugin-dialog = "2" # native Save/choose-folder (Phase 2 export) [target.'cfg(windows)'.dependencies] windows = { version = "0.58", features = [ diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 54bcf1e..35bb01d 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -3,5 +3,5 @@ "identifier": "default", "description": "Capability for the main window. App-defined commands (start_recording, etc.) are always invokable; this grants the core plugin permissions the frontend needs on top of that — window/event/app basics plus listening for recording://*, transcript://*, and the other Rust->UI events.", "windows": ["main"], - "permissions": ["core:default"] + "permissions": ["core:default", "dialog:default"] } diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json index 0eebfc4..f2f210a 100644 --- a/src-tauri/gen/schemas/acl-manifests.json +++ b/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","allow-supports-multiple-windows"]},"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-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"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-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"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-icon-with-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-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_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-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_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-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"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-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"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-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"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-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"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}} \ 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","allow-supports-multiple-windows"]},"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-supports-multiple-windows":{"identifier":"allow-supports-multiple-windows","description":"Enables the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":["supports_multiple_windows"],"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-supports-multiple-windows":{"identifier":"deny-supports-multiple-windows","description":"Denies the supports_multiple_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["supports_multiple_windows"]}},"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-icon-with-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-icon-with-as-template":{"identifier":"allow-set-icon-with-as-template","description":"Enables the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_with_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-icon-with-as-template":{"identifier":"deny-set-icon-with-as-template","description":"Denies the set_icon_with_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_with_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-activity-name","allow-scene-identifier","allow-internal-toggle-maximize"]},"permissions":{"allow-activity-name":{"identifier":"allow-activity-name","description":"Enables the activity_name command without any pre-configured scope.","commands":{"allow":["activity_name"],"deny":[]}},"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-scene-identifier":{"identifier":"allow-scene-identifier","description":"Enables the scene_identifier command without any pre-configured scope.","commands":{"allow":["scene_identifier"],"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-activity-name":{"identifier":"deny-activity-name","description":"Denies the activity_name command without any pre-configured scope.","commands":{"allow":[],"deny":["activity_name"]}},"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-scene-identifier":{"identifier":"deny-scene-identifier","description":"Denies the scene_identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["scene_identifier"]}},"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},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json index e0deb90..7d12dfe 100644 --- a/src-tauri/gen/schemas/capabilities.json +++ b/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"default":{"identifier":"default","description":"Capability for the main window. App-defined commands (start_recording, etc.) are always invokable; this grants the core plugin permissions the frontend needs on top of that — window/event/app basics plus listening for recording://*, transcript://*, and the other Rust->UI events.","local":true,"windows":["main"],"permissions":["core:default"]}} \ No newline at end of file +{"default":{"identifier":"default","description":"Capability for the main window. App-defined commands (start_recording, etc.) are always invokable; this grants the core plugin permissions the frontend needs on top of that — window/event/app basics plus listening for recording://*, transcript://*, and the other Rust->UI events.","local":true,"windows":["main"],"permissions":["core:default","dialog:default"]}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/desktop-schema.json b/src-tauri/gen/schemas/desktop-schema.json index 3286645..24c9001 100644 --- a/src-tauri/gen/schemas/desktop-schema.json +++ b/src-tauri/gen/schemas/desktop-schema.json @@ -2191,6 +2191,72 @@ "type": "string", "const": "core:window:deny-unminimize", "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." } ] }, diff --git a/src-tauri/gen/schemas/windows-schema.json b/src-tauri/gen/schemas/windows-schema.json index 3286645..24c9001 100644 --- a/src-tauri/gen/schemas/windows-schema.json +++ b/src-tauri/gen/schemas/windows-schema.json @@ -2191,6 +2191,72 @@ "type": "string", "const": "core:window:deny-unminimize", "markdownDescription": "Denies the unminimize command without any pre-configured scope." + }, + { + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "type": "string", + "const": "dialog:default", + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" + }, + { + "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-ask", + "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "type": "string", + "const": "dialog:allow-confirm", + "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + }, + { + "description": "Enables the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-message", + "markdownDescription": "Enables the message command without any pre-configured scope." + }, + { + "description": "Enables the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-open", + "markdownDescription": "Enables the open command without any pre-configured scope." + }, + { + "description": "Enables the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:allow-save", + "markdownDescription": "Enables the save command without any pre-configured scope." + }, + { + "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-ask", + "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "type": "string", + "const": "dialog:deny-confirm", + "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + }, + { + "description": "Denies the message command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-message", + "markdownDescription": "Denies the message command without any pre-configured scope." + }, + { + "description": "Denies the open command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-open", + "markdownDescription": "Denies the open command without any pre-configured scope." + }, + { + "description": "Denies the save command without any pre-configured scope.", + "type": "string", + "const": "dialog:deny-save", + "markdownDescription": "Denies the save command without any pre-configured scope." } ] }, diff --git a/src-tauri/src/audio/mod.rs b/src-tauri/src/audio/mod.rs index b5bdd70..5f21e8b 100644 --- a/src-tauri/src/audio/mod.rs +++ b/src-tauri/src/audio/mod.rs @@ -344,6 +344,66 @@ impl Resampler { } } +/// Read a WAV file written by this app's own `WavWriter` (Float32 or Int16; +/// see `write_wav_bytes`) and return mono `f32` samples at 16kHz — the batch +/// counterpart to the live capture path's downmix/resample, used for crash +/// recovery (`Transcriber::transcribe_file`, T2.8). +/// +/// A working WAV left behind by an abnormal exit never had `finalize()` patch +/// its RIFF/`data` chunk sizes, so they read back as 0 — exactly the file +/// recovery has to work from (FR-REL-1). Trusting hound's own duration/sample +/// iterator would silently see zero frames despite real PCM bytes on disk (and +/// feeding whisper.cpp zero samples hangs rather than erroring — verified). +/// So this reads the `data` payload as raw bytes directly off disk instead of +/// through hound's size-aware iterator; hound is only used to parse the +/// format/`fmt ` chunk. +pub fn read_wav_mono_16k(path: &Path) -> Result, AudioError> { + let reader = + hound::WavReader::open(path).map_err(|e| AudioError::Capture(format!("open wav: {e}")))?; + let spec = reader.spec(); + let channels = spec.channels.max(1) as usize; + drop(reader); + + let file = std::fs::read(path).map_err(|e| AudioError::Capture(format!("read wav: {e}")))?; + let data_marker = file + .windows(4) + .position(|w| w == b"data") + .ok_or_else(|| AudioError::Capture("wav has no data chunk".into()))?; + let payload_start = data_marker + 8; // "data" + 4-byte (possibly unfinalized) size field + let bytes = file.get(payload_start..).unwrap_or(&[]); + + let mono: Vec = match (spec.sample_format, spec.bits_per_sample) { + (SampleFormat::Float, 32) => bytes + .chunks_exact(4 * channels) + .map(|frame| { + frame + .chunks_exact(4) + .map(|c| f32::from_le_bytes(c.try_into().unwrap())) + .sum::() + / channels as f32 + }) + .collect(), + (SampleFormat::Int, 16) => bytes + .chunks_exact(2 * channels) + .map(|frame| { + frame + .chunks_exact(2) + .map(|c| i16::from_le_bytes(c.try_into().unwrap()) as f32 / i16::MAX as f32) + .sum::() + / channels as f32 + }) + .collect(), + (fmt, bits) => { + return Err(AudioError::Device(format!( + "unsupported wav format: {fmt:?} {bits}-bit" + ))) + } + }; + + let mut resampler = Resampler::new(spec.sample_rate); + Ok(resampler.process(&mono)) +} + #[cfg(test)] mod tests { use super::*; @@ -381,6 +441,73 @@ mod tests { let _ = std::fs::remove_dir_all(&dir); } + #[test] + fn read_wav_mono_16k_reads_a_normally_finalized_file() { + let dir = std::env::temp_dir().join(format!("wa-test-{}", uuid::Uuid::new_v4())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("audio.wav"); + + let spec = WavSpec { + channels: 1, + sample_rate: 16_000, + bits_per_sample: 32, + sample_format: SampleFormat::Float, + }; + let mut writer = WavWriter::create(&path, spec).unwrap(); + for i in 0..1600 { + writer.write_sample((i as f32 / 1600.0) - 0.5).unwrap(); + } + writer.finalize().unwrap(); + + let mono = read_wav_mono_16k(&path).unwrap(); + assert_eq!(mono.len(), 1600); // already 16kHz mono, no resampling needed + + let _ = std::fs::remove_dir_all(&dir); + } + + #[test] + fn read_wav_mono_16k_recovers_samples_from_an_unfinalized_crash_file() { + // hound's own `WavWriter` patches the RIFF/data sizes in both + // `finalize()` and `flush()`, and its `Drop` impl finalizes too — so + // there's no way to get it to leave an unfinalized file on disk short + // of `mem::forget`-ing mid-write, which depends on `BufWriter`'s + // internal (unspecified) flush threshold. Building the crashed file + // by hand instead gives a deterministic repro of exactly what a real + // crash leaves: a valid `fmt ` chunk with the RIFF/data sizes at 0. + let dir = std::env::temp_dir().join(format!("wa-test-{}", uuid::Uuid::new_v4())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("audio.wav"); + + let samples: Vec = (0..1600).map(|i| (i as f32 / 1600.0) - 0.5).collect(); + let pcm: Vec = samples.iter().flat_map(|s| s.to_le_bytes()).collect(); + + let mut file = Vec::new(); + file.extend_from_slice(b"RIFF"); + file.extend_from_slice(&0u32.to_le_bytes()); // unfinalized RIFF size + file.extend_from_slice(b"WAVE"); + file.extend_from_slice(b"fmt "); + file.extend_from_slice(&16u32.to_le_bytes()); // fmt chunk size + file.extend_from_slice(&3u16.to_le_bytes()); // WAVE_FORMAT_IEEE_FLOAT + file.extend_from_slice(&1u16.to_le_bytes()); // channels + file.extend_from_slice(&16_000u32.to_le_bytes()); // sample rate + file.extend_from_slice(&(16_000 * 4u32).to_le_bytes()); // byte rate + file.extend_from_slice(&4u16.to_le_bytes()); // block align + file.extend_from_slice(&32u16.to_le_bytes()); // bits per sample + file.extend_from_slice(b"data"); + file.extend_from_slice(&0u32.to_le_bytes()); // unfinalized data size + file.extend_from_slice(&pcm); + std::fs::write(&path, &file).unwrap(); + + let mono = read_wav_mono_16k(&path).unwrap(); + assert_eq!( + mono.len(), + 1600, + "should recover real samples despite the zeroed header" + ); + + let _ = std::fs::remove_dir_all(&dir); + } + #[test] fn decode_mono_f32_downmixes_stereo() { let format = WaveFormat::new(32, 32, &SampleType::Float, 48_000, 2, None); diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index ca65e63..15bed4c 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -8,10 +8,14 @@ use crate::audio::{AudioCapture, WasapiCapture}; use crate::error::WaResult; use crate::models::*; +use crate::notes::NotesRenderer; +use crate::paths::{meeting_dir, settings_path, wa_root, whisper_model_path}; +use crate::storage::{FinalizeMeeting, Meeting, NewMeeting}; use crate::transcription::{run_streaming_worker, Transcriber, WhisperTranscriber}; use crate::{error::WaError, AppState, RecordingSession}; use serde::Deserialize; use std::path::PathBuf; +use std::sync::{Arc, Mutex as StdMutex}; use tauri::{AppHandle, Emitter, State}; #[derive(Deserialize)] @@ -23,30 +27,9 @@ pub struct StartRecordingArgs { pub record: bool, } -// ---- Phase 1 file-backed settings + storage paths ---- -// Full SQLite persistence is Phase 2 (`storage::Store`, still `todo!()`); Phase -// 1 only needs `settings.json` (consent/default-retention, FR-REC-1/2) and the -// `meetings//` folder layout from `docs/03-data-model.md`. - -fn local_appdata() -> PathBuf { - PathBuf::from(std::env::var("LOCALAPPDATA").unwrap_or_else(|_| ".".to_string())) -} - -fn wa_root() -> PathBuf { - local_appdata().join("WhispAssist") -} - -fn settings_path() -> PathBuf { - wa_root().join("settings.json") -} - -fn meetings_dir() -> PathBuf { - wa_root().join("meetings") -} - -fn whisper_model_path() -> PathBuf { - wa_root().join("models").join("ggml-base.en-q5_1.bin") -} +// ---- File-backed settings (consent/default-retention/storage policy) ---- +// `settings.json` per `docs/03-data-model.md`; meeting rows/files go through +// `AppState.store` (Phase 2, `storage::SqliteStore`). fn default_settings() -> Settings { Settings { @@ -60,10 +43,12 @@ fn default_settings() -> Settings { default_record: false, consent_acknowledged: false, sync_enabled: false, + retention_max_age_days: None, + retention_max_size_gb: None, } } -fn load_settings() -> Settings { +pub(crate) fn load_settings() -> Settings { std::fs::read_to_string(settings_path()) .ok() .and_then(|s| serde_json::from_str(&s).ok()) @@ -114,10 +99,17 @@ pub async fn start_recording( )); } - let meeting_id = uuid::Uuid::new_v4().to_string(); - let meeting_dir = meetings_dir().join(&meeting_id); - std::fs::create_dir_all(&meeting_dir).map_err(|e| WaError::new("storage", e.to_string()))?; - let wav_path = meeting_dir.join("audio.wav"); + let meeting_id = state + .store + .create_meeting(NewMeeting { + title: args + .meeting_title + .unwrap_or_else(|| "Untitled meeting".to_string()), + calendar_event_id: args.calendar_event_id, + }) + .await + .map_err(|e| WaError::new("storage", e.to_string()))?; + let wav_path = meeting_dir(&meeting_id).join("audio.wav"); // Bounded so a slow/stalled transcription worker can never back up the capture thread. let (frame_tx, frame_rx) = std::sync::mpsc::sync_channel::>(8); @@ -125,6 +117,8 @@ pub async fn start_recording( .start(&wav_path, frame_tx) .map_err(|e| WaError::new("audio", e.to_string()))?; + let segments: Arc>> = Arc::new(StdMutex::new(Vec::new())); + let segments_for_worker = segments.clone(); let app_for_worker = app.clone(); let meeting_id_for_worker = meeting_id.clone(); let transcription_worker = std::thread::Builder::new() @@ -132,6 +126,9 @@ pub async fn start_recording( .spawn( move || match WhisperTranscriber::load(&model_path, BackendId::Cpu) { Ok(transcriber) => run_streaming_worker(&transcriber, frame_rx, |segment| { + if let Ok(mut buf) = segments_for_worker.lock() { + buf.push(segment.clone()); + } let _ = app_for_worker.emit( "transcript://segment", serde_json::json!({ "meetingId": meeting_id_for_worker, "segment": segment }), @@ -149,6 +146,7 @@ pub async fn start_recording( wav_path, started_at: std::time::Instant::now(), transcription_worker, + segments, }); drop(guard); @@ -193,13 +191,53 @@ pub async fn stop_recording( // guarantee it has fully drained the audio before we act on retention. let _ = session.transcription_worker.join(); + let segments = session + .segments + .lock() + .map(|g| g.clone()) + .unwrap_or_default(); + let segment_count = segments.len(); + // No diarization until Phase 4 — every segment is provisionally "S1". + let speakers = vec![SpeakerInfo { + label: "S1".to_string(), + display_name: None, + participant_id: None, + }]; + let model_used = whisper_model_path() + .file_stem() + .and_then(|s| s.to_str()) + .map(str::to_string); + + // T2.10: persist transcript.json (via finalize_meeting) and notes.md + // *before* touching the working WAV, so a crash here still leaves a + // recoverable, regenerable meeting. + state + .store + .finalize_meeting( + &meeting_id, + FinalizeMeeting { + segments: segments.clone(), + speakers: speakers.clone(), + duration_secs: (summary.duration_ms / 1000) as i64, + recorded: session.retention, + language: None, + backend_used: Some(BackendId::Cpu.as_str().to_string()), + model_used, + }, + ) + .await + .map_err(|e| WaError::new("storage", e.to_string()))?; + + let notes_md = crate::notes::MarkdownNotes.to_markdown(&segments, &speakers, None); + let _ = std::fs::write(meeting_dir(&meeting_id).join("notes.md"), notes_md); + let _ = app.emit( "transcript://finalized", - serde_json::json!({ "meetingId": meeting_id, "segmentCount": serde_json::Value::Null }), + serde_json::json!({ "meetingId": meeting_id, "segmentCount": segment_count }), ); - // ADR-0009: delete the working WAV only after the transcript worker has - // finished reading it, and only when retention is off. + // ADR-0009: delete the working WAV only after the transcript is finalized + // above, and only when retention is off. if !session.retention { let _ = std::fs::remove_file(&session.wav_path); } @@ -299,35 +337,191 @@ pub async fn hardware_status() -> WaResult> { todo!("Phase 3 — hardware_status") } +/// Re-run transcription from a `recovering` meeting's working `audio.wav` +/// (T2.8, FR-REL-1). CPU-bound, so it runs on a blocking task rather than +/// tying up an async worker. +#[tauri::command] +pub async fn resume_transcription( + app: AppHandle, + state: State<'_, AppState>, + meeting_id: MeetingId, +) -> WaResult<()> { + let model_path = whisper_model_path(); + if !model_path.exists() { + return Err(WaError::new( + "transcription", + format!( + "whisper model not found at {}; run `npm run download-models` first", + model_path.display() + ), + )); + } + let wav_path = meeting_dir(&meeting_id).join("audio.wav"); + if !wav_path.exists() { + return Err(WaError::new( + "recovery", + "no working audio.wav to recover from", + )); + } + + // Note: deliberately doesn't emit `recording://state` — that event drives + // the main header's single live-session Record/Stop toggle, and reusing + // it here would make the header show a phantom "Stop" button for a + // recovery run that isn't a `RecordingSession` at all. The "recovering" + // badge in the meetings list is this operation's own progress signal. + + let segments = tauri::async_runtime::spawn_blocking(move || { + WhisperTranscriber::load(&model_path, BackendId::Cpu) + .and_then(|transcriber| transcriber.transcribe_file(&wav_path)) + }) + .await + .map_err(|e| WaError::new("transcription", e.to_string()))? + .map_err(|e| WaError::new("transcription", e.to_string()))?; + + let speakers = vec![SpeakerInfo { + label: "S1".to_string(), + display_name: None, + participant_id: None, + }]; + let duration_secs = segments + .last() + .map(|s| (s.end_ms / 1000) as i64) + .unwrap_or(0); + let model_used = whisper_model_path() + .file_stem() + .and_then(|s| s.to_str()) + .map(str::to_string); + + state + .store + .finalize_meeting( + &meeting_id, + FinalizeMeeting { + segments: segments.clone(), + speakers: speakers.clone(), + duration_secs, + // The working WAV surviving a crash is the only signal we have + // left about intent; keep it rather than silently discard it. + recorded: true, + language: None, + backend_used: Some(BackendId::Cpu.as_str().to_string()), + model_used, + }, + ) + .await + .map_err(|e| WaError::new("storage", e.to_string()))?; + + let notes_md = crate::notes::MarkdownNotes.to_markdown(&segments, &speakers, None); + let _ = std::fs::write(meeting_dir(&meeting_id).join("notes.md"), notes_md); + + let _ = app.emit( + "transcript://finalized", + serde_json::json!({ "meetingId": meeting_id, "segmentCount": segments.len() }), + ); + Ok(()) +} + // ---- Meetings / storage (Phase 2) ---- #[tauri::command] -pub async fn list_meetings(_query: Option) -> WaResult> { - todo!("Phase 2 — list_meetings") +pub async fn list_meetings( + state: State<'_, AppState>, + query: Option, +) -> WaResult> { + state + .store + .list_meetings(query) + .await + .map_err(|e| WaError::new("storage", e.to_string())) } #[tauri::command] -pub async fn get_meeting(_meeting_id: MeetingId) -> WaResult { - todo!("Phase 2 — get_meeting (returns full Meeting incl. transcript)") +pub async fn get_meeting(state: State<'_, AppState>, meeting_id: MeetingId) -> WaResult { + state + .store + .get_meeting(&meeting_id) + .await + .map_err(|e| WaError::new("storage", e.to_string())) } #[tauri::command] -pub async fn delete_meeting(_meeting_id: MeetingId) -> WaResult<()> { - todo!("Phase 2 — delete_meeting") +pub async fn delete_meeting(state: State<'_, AppState>, meeting_id: MeetingId) -> WaResult<()> { + let guard = state.session.lock().await; + if guard.as_ref().is_some_and(|s| s.meeting_id == meeting_id) { + return Err(WaError::new( + "recording", + "cannot delete a meeting that is currently recording", + )); + } + drop(guard); + state + .store + .delete_meeting(&meeting_id) + .await + .map_err(|e| WaError::new("storage", e.to_string())) } #[tauri::command] -pub async fn update_notes(_meeting_id: MeetingId, _markdown: String) -> WaResult<()> { - todo!("Phase 2 — update_notes") +pub async fn update_notes( + state: State<'_, AppState>, + meeting_id: MeetingId, + markdown: String, +) -> WaResult<()> { + state + .store + .update_notes(&meeting_id, &markdown) + .await + .map_err(|e| WaError::new("storage", e.to_string())) } +/// `format` ∈ `md | bundle` (PDF/Word stay Phase 8). `dest` is a file path for +/// `md`, a destination folder for `bundle` (the frontend gets it from a native +/// Save/choose-folder dialog). #[tauri::command] pub async fn export_meeting( - _meeting_id: MeetingId, - _dest: String, - _format: String, + state: State<'_, AppState>, + meeting_id: MeetingId, + dest: String, + format: String, ) -> WaResult { - todo!("Phase 2/8 — export_meeting (md|pdf|docx|bundle)") + let meeting = state + .store + .get_meeting(&meeting_id) + .await + .map_err(|e| WaError::new("storage", e.to_string()))?; + let dest_path = PathBuf::from(&dest); + match format.as_str() { + "md" => { + crate::notes::MarkdownNotes + .export( + &meeting.notes_markdown, + &dest_path, + crate::notes::ExportFormat::Md, + ) + .map_err(|e| WaError::new("export", e.to_string()))?; + } + "bundle" => { + std::fs::create_dir_all(&dest_path) + .map_err(|e| WaError::new("export", e.to_string()))?; + let source_dir = meeting_dir(&meeting_id); + for name in ["audio.wav", "transcript.json"] { + let src = source_dir.join(name); + if src.exists() { + std::fs::copy(&src, dest_path.join(name)) + .map_err(|e| WaError::new("export", e.to_string()))?; + } + } + std::fs::write(dest_path.join("notes.md"), &meeting.notes_markdown) + .map_err(|e| WaError::new("export", e.to_string()))?; + } + other => { + return Err(WaError::new( + "export", + format!("unsupported export format: {other}"), + )) + } + } + Ok(dest) } // ---- LLM (Phase 5) ---- diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ac9d99f..dd84ccc 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -15,11 +15,13 @@ pub mod llm; pub mod mcp; pub mod models; pub mod notes; +pub mod paths; pub mod storage; pub mod sync; pub mod transcription; use std::path::PathBuf; +use std::sync::{Arc, Mutex as StdMutex}; use std::thread::JoinHandle; use tauri::tray::TrayIcon; use tauri::Manager; @@ -27,12 +29,11 @@ use tokio::sync::Mutex; /// Shared application state handed to every Tauri command via `tauri::State`. /// -/// Phase 1 only needs the in-flight recording session. `hardware`/`store`/ -/// `llm`/`sync`/`mcp` service handles join this struct once their concrete -/// implementations exist (Phase 2/3/5/9/10) — wiring them in ahead of that -/// isn't possible yet (several don't have a concrete impl to construct) and -/// would just be unused plumbing until then. +/// `hardware`/`llm`/`sync`/`mcp` service handles join this struct once their +/// concrete implementations exist (Phase 3/5/9/10) — several don't have one to +/// construct yet, so wiring them in early would just be unused plumbing. pub struct AppState { + pub store: Arc, /// The single in-flight recording, if any. Guarded so start/stop/pause are atomic. pub session: Mutex>, } @@ -46,6 +47,10 @@ pub struct RecordingSession { pub wav_path: PathBuf, pub started_at: std::time::Instant, pub transcription_worker: JoinHandle<()>, + /// Segments produced so far, appended by the same closure that emits + /// `transcript://segment` — lets `stop_recording` persist the full + /// transcript without a second round-trip through the event channel. + pub segments: Arc>>, } /// Wraps the tray icon so it can be looked up from commands to update its @@ -56,17 +61,47 @@ pub struct TrayHandle(pub TrayIcon); pub fn run() { tracing_subscriber::fmt().with_env_filter("info").init(); + let store: Arc = Arc::new( + tauri::async_runtime::block_on(storage::SqliteStore::connect()) + .expect("failed to initialize storage (wa.db)"), + ); + let store_for_setup = store.clone(); + tauri::Builder::default() + .plugin(tauri_plugin_dialog::init()) .manage(AppState { + store, session: Mutex::new(None), }) - .setup(|app| { + .setup(move |app| { let icon = tauri::image::Image::from_bytes(include_bytes!("../icons/tray.png"))?; let tray = tauri::tray::TrayIconBuilder::new() .icon(icon) .tooltip("WhispAssist — idle") .build(app)?; app.manage(TrayHandle(tray)); + + // Startup recovery + retention pass (FR-REL-1, FR-STORE-2). Spawned so + // it never blocks the window from showing (NFR-PERF-4); nothing here + // repeats on a timer (NFR-RES-1). + let store = store_for_setup; + tauri::async_runtime::spawn(async move { + match store.recover_scan().await { + Ok(recovered) if !recovered.is_empty() => { + tracing::info!("recovered {} interrupted meeting(s)", recovered.len()); + } + Ok(_) => {} + Err(e) => tracing::error!("startup recovery scan failed: {e}"), + } + let settings = commands::load_settings(); + let policy = storage::Retention { + max_age_days: settings.retention_max_age_days, + max_size_gb: settings.retention_max_size_gb, + }; + if let Err(e) = store.enforce_retention(policy).await { + tracing::error!("startup retention enforcement failed: {e}"); + } + }); Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -76,6 +111,7 @@ pub fn run() { commands::resume_recording, commands::set_recording_retention, commands::acknowledge_recording_consent, + commands::resume_transcription, commands::hardware_status, commands::list_meetings, commands::get_meeting, diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index 446366f..7cc8c07 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -15,6 +15,18 @@ pub enum BackendId { Cpu, } +impl BackendId { + pub fn as_str(&self) -> &'static str { + match self { + BackendId::Npu => "npu", + BackendId::Nvidia => "nvidia", + BackendId::Amd => "amd", + BackendId::Intel => "intel", + BackendId::Cpu => "cpu", + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct BackendInfo { pub id: BackendId, @@ -25,7 +37,7 @@ pub struct BackendInfo { pub vram_mb: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "lowercase")] pub enum MeetingStatus { Recording, @@ -35,6 +47,31 @@ pub enum MeetingStatus { Error, } +impl MeetingStatus { + pub fn as_str(&self) -> &'static str { + match self { + MeetingStatus::Recording => "recording", + MeetingStatus::Transcribing => "transcribing", + MeetingStatus::Ready => "ready", + MeetingStatus::Recovering => "recovering", + MeetingStatus::Error => "error", + } + } + + /// Parses a `meetings.status` DB value. Unrecognized values map to `Error` + /// rather than panicking — a stored value should always be one written by + /// this app, but a row is not worth crashing the app over. + pub fn parse(s: &str) -> Self { + match s { + "recording" => MeetingStatus::Recording, + "transcribing" => MeetingStatus::Transcribing, + "ready" => MeetingStatus::Ready, + "recovering" => MeetingStatus::Recovering, + _ => MeetingStatus::Error, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TranscriptSegment { pub id: u64, @@ -112,6 +149,9 @@ pub struct Settings { pub consent_acknowledged: bool, // Sync master switch (ADR-0010). Default OFF. Target rows live in the DB; secrets in OS keychain. pub sync_enabled: bool, + // Storage retention policy (FR-STORE-2). None = no cap on that dimension. + pub retention_max_age_days: Option, + pub retention_max_size_gb: Option, } // ---- Sync (ADR-0010) ---- diff --git a/src-tauri/src/notes/mod.rs b/src-tauri/src/notes/mod.rs index 291bf1a..f8d1a7e 100644 --- a/src-tauri/src/notes/mod.rs +++ b/src-tauri/src/notes/mod.rs @@ -42,20 +42,131 @@ pub struct MarkdownNotes; impl NotesRenderer for MarkdownNotes { fn to_markdown( &self, - _segments: &[TranscriptSegment], - _speakers: &[SpeakerInfo], - _summary_md: Option<&str>, + segments: &[TranscriptSegment], + speakers: &[SpeakerInfo], + summary_md: Option<&str>, ) -> String { - // T2.4: resolve speaker names, group dialogue, prepend summary section. - todo!("Phase 2 — assemble Markdown") + let mut out = String::new(); + if let Some(summary) = summary_md { + let summary = summary.trim(); + if !summary.is_empty() { + out.push_str(summary); + out.push_str("\n\n---\n\n"); + } + } + + let name_for = |label: &str| -> String { + speakers + .iter() + .find(|s| s.label == label) + .and_then(|s| s.display_name.clone()) + .unwrap_or_else(|| label.to_string()) + }; + + // Group consecutive segments from the same speaker into one paragraph + // (matters once Phase 4 diarization produces more than one speaker). + let mut current_speaker: Option<&str> = None; + let mut buffer = String::new(); + for seg in segments { + let text = seg.text.trim(); + if text.is_empty() { + continue; + } + if current_speaker != Some(seg.speaker.as_str()) { + if let Some(speaker) = current_speaker { + out.push_str(&format!("**{}:** {}\n\n", name_for(speaker), buffer.trim())); + } + current_speaker = Some(seg.speaker.as_str()); + buffer.clear(); + } + if !buffer.is_empty() { + buffer.push(' '); + } + buffer.push_str(text); + } + if let Some(speaker) = current_speaker { + out.push_str(&format!("**{}:** {}\n\n", name_for(speaker), buffer.trim())); + } + + out.trim_end().to_string() } + fn export( &self, - _markdown: &str, - _dest: &Path, - _fmt: ExportFormat, + markdown: &str, + dest: &Path, + fmt: ExportFormat, ) -> Result { - // T2.6 (.md/bundle); T8.4 (pdf/docx via local conversion). - todo!("Phase 2/8 — export notes") + match fmt { + ExportFormat::Md => { + if let Some(parent) = dest.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(dest, markdown)?; + Ok(dest.to_path_buf()) + } + // Bundling pulls in audio.wav/transcript.json alongside notes.md — + // that's meeting-folder orchestration, not Markdown rendering, so + // `commands::export_meeting` handles it directly rather than here. + ExportFormat::Bundle => Err(NotesError::Export( + "bundle export is assembled in commands::export_meeting, not NotesRenderer".into(), + )), + ExportFormat::Pdf | ExportFormat::Docx => Err(NotesError::Export( + "PDF/Word export lands in Phase 8".into(), + )), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn seg(id: u64, speaker: &str, text: &str) -> TranscriptSegment { + TranscriptSegment { + id, + start_ms: id * 1000, + end_ms: id * 1000 + 900, + speaker: speaker.to_string(), + text: text.to_string(), + confidence: Some(0.9), + interim: false, + } + } + + #[test] + fn groups_consecutive_same_speaker_segments_into_one_paragraph() { + let segments = vec![ + seg(0, "S1", "Hello"), + seg(1, "S1", "world."), + seg(2, "S2", "Hi there."), + ]; + let md = MarkdownNotes.to_markdown(&segments, &[], None); + assert_eq!(md, "**S1:** Hello world.\n\n**S2:** Hi there."); + } + + #[test] + fn resolves_display_name_and_falls_back_to_label() { + let segments = vec![seg(0, "S1", "Hi")]; + let speakers = [SpeakerInfo { + label: "S1".to_string(), + display_name: Some("Alex".to_string()), + participant_id: None, + }]; + assert_eq!( + MarkdownNotes.to_markdown(&segments, &speakers, None), + "**Alex:** Hi" + ); + assert_eq!( + MarkdownNotes.to_markdown(&segments, &[], None), + "**S1:** Hi" + ); + } + + #[test] + fn skips_blank_segments_and_prepends_summary() { + let segments = vec![seg(0, "S1", " "), seg(1, "S1", "Real text.")]; + let md = MarkdownNotes.to_markdown(&segments, &[], Some("## Summary\nDone.")); + assert_eq!(md, "## Summary\nDone.\n\n---\n\n**S1:** Real text."); } } diff --git a/src-tauri/src/paths.rs b/src-tauri/src/paths.rs new file mode 100644 index 0000000..dbba9e5 --- /dev/null +++ b/src-tauri/src/paths.rs @@ -0,0 +1,34 @@ +//! Shared on-disk layout helpers (`docs/03-data-model.md`). Used by `storage`, +//! `commands`, and `lib` so the root/meetings/models/settings paths are computed +//! exactly one way. + +use crate::models::MeetingId; +use std::path::PathBuf; + +pub fn local_appdata() -> PathBuf { + PathBuf::from(std::env::var("LOCALAPPDATA").unwrap_or_else(|_| ".".to_string())) +} + +pub fn wa_root() -> PathBuf { + local_appdata().join("WhispAssist") +} + +pub fn db_path() -> PathBuf { + wa_root().join("wa.db") +} + +pub fn settings_path() -> PathBuf { + wa_root().join("settings.json") +} + +pub fn meetings_dir() -> PathBuf { + wa_root().join("meetings") +} + +pub fn meeting_dir(id: &MeetingId) -> PathBuf { + meetings_dir().join(id) +} + +pub fn whisper_model_path() -> PathBuf { + wa_root().join("models").join("ggml-base.en-q5_1.bin") +} diff --git a/src-tauri/src/storage/mod.rs b/src-tauri/src/storage/mod.rs index 1b94c0b..bc755ba 100644 --- a/src-tauri/src/storage/mod.rs +++ b/src-tauri/src/storage/mod.rs @@ -4,8 +4,14 @@ //! Invariant: audio is the source of truth. Derived artifacts (transcript/notes/ //! summary) are regenerable; retention never touches an in-progress meeting. -use crate::models::{MeetingId, MeetingListItem}; +use crate::models::{MeetingId, MeetingListItem, MeetingStatus, SpeakerInfo, TranscriptSegment}; +use crate::paths; use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use sqlx::sqlite::{SqliteConnectOptions, SqlitePool, SqlitePoolOptions}; +use sqlx::Row; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; #[derive(Debug, thiserror::Error)] pub enum StoreError { @@ -17,11 +23,49 @@ pub enum StoreError { NotFound(String), } +impl From for StoreError { + fn from(e: sqlx::Error) -> Self { + StoreError::Db(e.to_string()) + } +} + pub struct NewMeeting { pub title: String, pub calendar_event_id: Option, } +/// What actually happened during the session — richer than the doc's bare +/// `&MeetingId` because `finalize_meeting` has to persist what was captured. +pub struct FinalizeMeeting { + pub segments: Vec, + pub speakers: Vec, + pub duration_secs: i64, + pub recorded: bool, + pub language: Option, + pub backend_used: Option, + pub model_used: Option, +} + +/// Full meeting detail: DB row + transcript + speakers + notes (`get_meeting`'s +/// documented contract — `docs/04-api-contracts.md` — which the old +/// `MeetingListItem`-only stub couldn't represent). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Meeting { + pub id: MeetingId, + pub title: String, + pub started_at: i64, + pub ended_at: Option, + pub duration_secs: Option, + pub status: MeetingStatus, + pub recorded: bool, + pub language: Option, + pub backend_used: Option, + pub model_used: Option, + pub segments: Vec, + pub speakers: Vec, + pub notes_markdown: String, +} + pub struct Retention { pub max_age_days: Option, pub max_size_gb: Option, @@ -30,51 +74,346 @@ pub struct Retention { #[async_trait] pub trait Store: Send + Sync { async fn create_meeting(&self, m: NewMeeting) -> Result; - async fn finalize_meeting(&self, id: &MeetingId) -> Result<(), StoreError>; + async fn finalize_meeting(&self, id: &MeetingId, s: FinalizeMeeting) -> Result<(), StoreError>; async fn list_meetings( &self, query: Option, ) -> Result, StoreError>; + async fn get_meeting(&self, id: &MeetingId) -> Result; async fn delete_meeting(&self, id: &MeetingId) -> Result<(), StoreError>; async fn update_notes(&self, id: &MeetingId, markdown: &str) -> Result<(), StoreError>; /// Full-text search across transcripts + notes (Phase 8, FR-SEARCH-1). async fn search(&self, query: &str) -> Result, StoreError>; /// Startup reconcile: meetings with audio but no finalized transcript (FR-REL-1). async fn recover_scan(&self) -> Result, StoreError>; - /// Enforce retention; returns count removed. Skips in-progress meetings. + /// Enforce retention; returns count removed. Skips in-progress meetings + /// (anything whose `status` isn't a terminal `ready`/`error`). async fn enforce_retention(&self, policy: Retention) -> Result; } -/// SQLite-backed store. Migrations live in `migrations/` (sqlx::migrate!). -pub struct SqliteStore; +/// SQLite-backed store. Migrations live in `migrations/` (`sqlx::migrate!`). +pub struct SqliteStore { + pool: SqlitePool, +} + +impl SqliteStore { + pub async fn connect() -> Result { + let root = paths::wa_root(); + std::fs::create_dir_all(&root)?; + let opts = SqliteConnectOptions::new() + .filename(paths::db_path()) + .create_if_missing(true) + .foreign_keys(true); + let pool = SqlitePoolOptions::new() + .max_connections(5) + .connect_with(opts) + .await?; + sqlx::migrate!("./migrations") + .run(&pool) + .await + .map_err(|e| StoreError::Db(e.to_string()))?; + Ok(Self { pool }) + } +} + +fn now_unix() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs() as i64) + .unwrap_or(0) +} + +/// On-disk shape of `transcript.json` (`docs/03-data-model.md`). +#[derive(Debug, Serialize, Deserialize, Default)] +struct TranscriptFile { + schema: u32, + meeting_id: MeetingId, + language: Option, + model: Option, + backend: Option, + segments: Vec, + speakers: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +struct TranscriptSpeaker { + label: String, + display_name: Option, +} + +fn row_to_list_item(row: &sqlx::sqlite::SqliteRow) -> MeetingListItem { + MeetingListItem { + id: row.get("id"), + title: row.get("title"), + started_at: row.get("started_at"), + duration_secs: row.get("duration_secs"), + status: MeetingStatus::parse(row.get::("status").as_str()), + } +} #[async_trait] impl Store for SqliteStore { - async fn create_meeting(&self, _m: NewMeeting) -> Result { - todo!("Phase 2 — create meeting row + folder") + async fn create_meeting(&self, m: NewMeeting) -> Result { + let id = uuid::Uuid::new_v4().to_string(); + let folder = paths::meeting_dir(&id); + std::fs::create_dir_all(&folder)?; + let audio_path = folder.join("audio.wav"); + let now = now_unix(); + sqlx::query( + "INSERT INTO meetings (id, title, started_at, folder_path, audio_path, status, calendar_event_id, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, 'recording', ?, ?, ?)", + ) + .bind(&id) + .bind(&m.title) + .bind(now) + .bind(folder.display().to_string()) + .bind(audio_path.display().to_string()) + .bind(&m.calendar_event_id) + .bind(now) + .bind(now) + .execute(&self.pool) + .await?; + Ok(id) } - async fn finalize_meeting(&self, _id: &MeetingId) -> Result<(), StoreError> { - todo!("Phase 2 — persist transcript.json + metadata") + + async fn finalize_meeting(&self, id: &MeetingId, s: FinalizeMeeting) -> Result<(), StoreError> { + let now = now_unix(); + sqlx::query( + "UPDATE meetings SET status = 'ready', ended_at = ?, duration_secs = ?, recorded = ?, + language = ?, backend_used = ?, model_used = ?, updated_at = ? WHERE id = ?", + ) + .bind(now) + .bind(s.duration_secs) + .bind(s.recorded as i64) + .bind(&s.language) + .bind(&s.backend_used) + .bind(&s.model_used) + .bind(now) + .bind(id) + .execute(&self.pool) + .await?; + + for speaker in &s.speakers { + sqlx::query( + "INSERT INTO speakers (id, meeting_id, label, display_name) + VALUES (?, ?, ?, ?) + ON CONFLICT(meeting_id, label) DO UPDATE SET display_name = excluded.display_name", + ) + .bind(uuid::Uuid::new_v4().to_string()) + .bind(id) + .bind(&speaker.label) + .bind(&speaker.display_name) + .execute(&self.pool) + .await?; + } + + let transcript = TranscriptFile { + schema: 1, + meeting_id: id.clone(), + language: s.language.clone(), + model: s.model_used.clone(), + backend: s.backend_used.clone(), + segments: s.segments, + speakers: s + .speakers + .iter() + .map(|sp| TranscriptSpeaker { + label: sp.label.clone(), + display_name: sp.display_name.clone(), + }) + .collect(), + }; + let json = + serde_json::to_string_pretty(&transcript).map_err(|e| StoreError::Db(e.to_string()))?; + std::fs::write(paths::meeting_dir(id).join("transcript.json"), json)?; + Ok(()) } + async fn list_meetings( &self, - _query: Option, + query: Option, ) -> Result, StoreError> { - todo!("Phase 2 — list meetings") + let rows = match query.filter(|q| !q.trim().is_empty()) { + Some(q) => { + let like = format!("%{}%", q.trim()); + sqlx::query("SELECT id, title, started_at, duration_secs, status FROM meetings WHERE title LIKE ? ORDER BY started_at DESC") + .bind(like) + .fetch_all(&self.pool) + .await? + } + None => { + sqlx::query("SELECT id, title, started_at, duration_secs, status FROM meetings ORDER BY started_at DESC") + .fetch_all(&self.pool) + .await? + } + }; + Ok(rows.iter().map(row_to_list_item).collect()) } - async fn delete_meeting(&self, _id: &MeetingId) -> Result<(), StoreError> { - todo!("Phase 2 — delete meeting + folder") + + async fn get_meeting(&self, id: &MeetingId) -> Result { + let row = sqlx::query( + "SELECT id, title, started_at, ended_at, duration_secs, status, recorded, language, backend_used, model_used + FROM meetings WHERE id = ?", + ) + .bind(id) + .fetch_optional(&self.pool) + .await? + .ok_or_else(|| StoreError::NotFound(id.clone()))?; + + let speaker_rows = sqlx::query( + "SELECT label, display_name FROM speakers WHERE meeting_id = ? ORDER BY label", + ) + .bind(id) + .fetch_all(&self.pool) + .await?; + let speakers: Vec = speaker_rows + .iter() + .map(|r| SpeakerInfo { + label: r.get("label"), + display_name: r.get("display_name"), + participant_id: None, + }) + .collect(); + + let folder = paths::meeting_dir(id); + let segments = std::fs::read_to_string(folder.join("transcript.json")) + .ok() + .and_then(|s| serde_json::from_str::(&s).ok()) + .map(|t| t.segments) + .unwrap_or_default(); + let notes_markdown = std::fs::read_to_string(folder.join("notes.md")).unwrap_or_default(); + + Ok(Meeting { + id: row.get("id"), + title: row.get("title"), + started_at: row.get("started_at"), + ended_at: row.get("ended_at"), + duration_secs: row.get("duration_secs"), + status: MeetingStatus::parse(row.get::("status").as_str()), + recorded: row.get::("recorded") != 0, + language: row.get("language"), + backend_used: row.get("backend_used"), + model_used: row.get("model_used"), + segments, + speakers, + notes_markdown, + }) } - async fn update_notes(&self, _id: &MeetingId, _markdown: &str) -> Result<(), StoreError> { - todo!("Phase 2 — write notes.md + FTS") + + async fn delete_meeting(&self, id: &MeetingId) -> Result<(), StoreError> { + sqlx::query("DELETE FROM meetings WHERE id = ?") + .bind(id) + .execute(&self.pool) + .await?; + let folder = paths::meeting_dir(id); + if folder.exists() { + std::fs::remove_dir_all(folder)?; + } + Ok(()) } + + async fn update_notes(&self, id: &MeetingId, markdown: &str) -> Result<(), StoreError> { + std::fs::write(paths::meeting_dir(id).join("notes.md"), markdown)?; + sqlx::query("UPDATE meetings SET updated_at = ? WHERE id = ?") + .bind(now_unix()) + .bind(id) + .execute(&self.pool) + .await?; + Ok(()) + } + async fn search(&self, _query: &str) -> Result, StoreError> { todo!("Phase 8 — FTS5 search") } + async fn recover_scan(&self) -> Result, StoreError> { - todo!("Phase 2 — recovery scan") + let rows = sqlx::query( + "SELECT id, audio_path FROM meetings WHERE status IN ('recording', 'transcribing')", + ) + .fetch_all(&self.pool) + .await?; + let mut recovered = Vec::new(); + for row in rows { + let id: MeetingId = row.get("id"); + let audio_path: String = row.get("audio_path"); + if PathBuf::from(&audio_path).exists() { + sqlx::query( + "UPDATE meetings SET status = 'recovering', updated_at = ? WHERE id = ?", + ) + .bind(now_unix()) + .bind(&id) + .execute(&self.pool) + .await?; + recovered.push(id); + } else { + // No working audio and never finalized: nothing to recover from. + sqlx::query("UPDATE meetings SET status = 'error', updated_at = ? WHERE id = ?") + .bind(now_unix()) + .bind(&id) + .execute(&self.pool) + .await?; + } + } + Ok(recovered) } - async fn enforce_retention(&self, _policy: Retention) -> Result { - todo!("Phase 2 — retention enforcement") + + async fn enforce_retention(&self, policy: Retention) -> Result { + if policy.max_age_days.is_none() && policy.max_size_gb.is_none() { + return Ok(0); + } + let candidates = sqlx::query( + "SELECT id, folder_path, started_at FROM meetings WHERE status IN ('ready', 'error') ORDER BY started_at ASC", + ) + .fetch_all(&self.pool) + .await?; + + let mut removed = 0u32; + let now = now_unix(); + let max_age_secs = policy.max_age_days.map(|d| d as i64 * 86_400); + + // Age-based pruning first (cheap, no disk walk needed). + let mut kept = Vec::new(); + for row in &candidates { + let id: MeetingId = row.get("id"); + let started_at: i64 = row.get("started_at"); + if let Some(max_age) = max_age_secs { + if now - started_at > max_age { + self.delete_meeting(&id).await?; + removed += 1; + continue; + } + } + kept.push((id, row.get::("folder_path"))); + } + + // Size-based pruning, oldest first, until under the cap. + if let Some(max_gb) = policy.max_size_gb { + let cap_bytes = max_gb as u64 * 1_073_741_824; + let sizes: Vec<(MeetingId, u64)> = kept + .iter() + .map(|(id, folder)| (id.clone(), dir_size(folder))) + .collect(); + let mut total: u64 = sizes.iter().map(|(_, s)| s).sum(); + let mut idx = 0; + while total > cap_bytes && idx < sizes.len() { + let (id, size) = &sizes[idx]; + self.delete_meeting(id).await?; + total = total.saturating_sub(*size); + removed += 1; + idx += 1; + } + } + + Ok(removed) } } + +fn dir_size(path: &str) -> u64 { + std::fs::read_dir(path) + .into_iter() + .flatten() + .flatten() + .filter_map(|entry| entry.metadata().ok()) + .map(|m| m.len()) + .sum() +} diff --git a/src-tauri/src/transcription/mod.rs b/src-tauri/src/transcription/mod.rs index 9cf0f4a..31cbc3f 100644 --- a/src-tauri/src/transcription/mod.rs +++ b/src-tauri/src/transcription/mod.rs @@ -31,7 +31,8 @@ pub trait Transcriber: Send + Sync { Self: Sized; /// Streaming: emit interim + final segments for a window (FR-TRX-2). fn transcribe_stream(&self, audio: AudioWindow, out: SegmentSink) -> Result<(), TrxError>; - /// Batch: one-shot, higher accuracy (FR-TRX-3). + /// Batch: one-shot over a whole file, higher accuracy — also the crash-recovery + /// path (FR-TRX-3, T2.8): re-run over the working `audio.wav` from scratch. fn transcribe_file(&self, wav: &Path) -> Result, TrxError>; } @@ -42,6 +43,88 @@ pub struct WhisperTranscriber { next_id: std::sync::atomic::AtomicU64, } +#[cfg(feature = "cpu-transcription")] +impl WhisperTranscriber { + /// Shared decode path for both streaming windows and batch files. + /// + /// `single_segment` forces whisper.cpp to treat the whole buffer as one + /// segment — needed for the short (~4s) streaming windows (see + /// `transcribe_stream`'s doc comment for why), but wrong for batch/file + /// transcription: a multi-minute recording should get whisper's normal + /// speech-boundary-driven multi-segment output, and its internal 30s-chunk + /// seek loop handles audio longer than one chunk on its own. + fn run_full( + &self, + samples: &[f32], + offset_ms: u64, + single_segment: bool, + ) -> Result, TrxError> { + // whisper.cpp hangs (not errors) on an empty buffer — verified against + // a crash-recovered WAV whose header hadn't been finalized. Belt and + // suspenders alongside `read_wav_mono_16k` reading raw bytes directly. + if samples.is_empty() { + return Ok(Vec::new()); + } + + let mut state = self + .ctx + .create_state() + .map_err(|e| TrxError::Inference(e.to_string()))?; + + // best_of: 1 — fastest greedy decode, matching the CPU/typical-laptop + // baseline (NFR-PERF-3). + let mut params = + whisper_rs::FullParams::new(whisper_rs::SamplingStrategy::Greedy { best_of: 1 }); + params.set_n_threads(available_threads()); + params.set_translate(false); + params.set_print_progress(false); + params.set_print_realtime(false); + params.set_print_timestamps(false); + params.set_suppress_blank(true); + params.set_single_segment(single_segment); + + state + .full(params, samples) + .map_err(|e| TrxError::Inference(e.to_string()))?; + + // A forced single_segment's reported end_timestamp() reflects + // whisper.cpp's internal 30s-padded mel frame, not the real window + // length — confirmed even with `duration_ms` set, so don't trust it. + // We already know the window's true length from `samples`, which is + // the more reliable source for `single_segment` windows; whisper's own + // multi-segment timestamps (batch/`transcribe_file`) stay authoritative. + let window_end_ms = offset_ms + (samples.len() as u64 * 1000) / 16_000; + + let mut out = Vec::new(); + for seg in state.as_iter() { + let text = seg.to_str_lossy().unwrap_or_default().trim().to_string(); + if text.is_empty() { + continue; + } + // Whisper timestamps are centiseconds (10ms units). + let start_ms = offset_ms + seg.start_timestamp().max(0) as u64 * 10; + let end_ms = if single_segment { + window_end_ms.max(start_ms) + } else { + offset_ms + seg.end_timestamp().max(0) as u64 * 10 + }; + out.push(TranscriptSegment { + id: self + .next_id + .fetch_add(1, std::sync::atomic::Ordering::SeqCst), + start_ms, + end_ms, + // Diarization lands in Phase 4; every segment is provisionally "S1" until then. + speaker: "S1".to_string(), + text, + confidence: Some((1.0 - seg.no_speech_probability()).clamp(0.0, 1.0)), + interim: false, + }); + } + Ok(out) + } +} + #[cfg(feature = "cpu-transcription")] impl Transcriber for WhisperTranscriber { fn load(model: &Path, _backend: BackendId) -> Result { @@ -57,55 +140,13 @@ impl Transcriber for WhisperTranscriber { } fn transcribe_stream(&self, audio: AudioWindow, out: SegmentSink) -> Result<(), TrxError> { - let mut state = self - .ctx - .create_state() - .map_err(|e| TrxError::Inference(e.to_string()))?; - - // best_of: 1 — fastest greedy decode, matching the CPU/typical-laptop - // baseline (NFR-PERF-3); real-time windowed transcription favors low - // latency per window over the extra accuracy of a wider search. - let mut params = - whisper_rs::FullParams::new(whisper_rs::SamplingStrategy::Greedy { best_of: 1 }); - params.set_n_threads(available_threads()); - params.set_translate(false); - params.set_print_progress(false); - params.set_print_realtime(false); - params.set_print_timestamps(false); - params.set_suppress_blank(true); // Each call already gets exactly one fixed-size window (not a full 30s - // clip); without this, whisper.cpp's segment-boundary heuristic hits - // "single timestamp ending" on short buffers and silently drops the - // whole window even when decoding succeeded (verified against real - // audio: text decoded correctly but zero segments came out until this - // was set — see whisper.cpp's own `stream` example, which sets this - // for the same reason). - params.set_single_segment(true); - - state - .full(params, &audio.samples) - .map_err(|e| TrxError::Inference(e.to_string()))?; - - for seg in state.as_iter() { - let text = seg.to_str_lossy().unwrap_or_default().trim().to_string(); - if text.is_empty() { - continue; - } - // Whisper timestamps are centiseconds (10ms units). - let start_ms = audio.offset_ms + seg.start_timestamp().max(0) as u64 * 10; - let end_ms = audio.offset_ms + seg.end_timestamp().max(0) as u64 * 10; - let segment = TranscriptSegment { - id: self - .next_id - .fetch_add(1, std::sync::atomic::Ordering::SeqCst), - start_ms, - end_ms, - // Diarization lands in Phase 4; every segment is provisionally "S1" until then. - speaker: "S1".to_string(), - text, - confidence: Some((1.0 - seg.no_speech_probability()).clamp(0.0, 1.0)), - interim: false, - }; + // clip); without `single_segment: true`, whisper.cpp's segment-boundary + // heuristic hits "single timestamp ending" on short buffers and drops + // the whole window even when decoding succeeded (verified against real + // audio — see whisper.cpp's own `stream` example, which sets this for + // the same reason). + for segment in self.run_full(&audio.samples, audio.offset_ms, true)? { if out.send(segment).is_err() { break; // receiver gone (meeting stopped) — nothing left to do } @@ -113,8 +154,10 @@ impl Transcriber for WhisperTranscriber { Ok(()) } - fn transcribe_file(&self, _wav: &Path) -> Result, TrxError> { - todo!("Phase 3 — batch transcription") + fn transcribe_file(&self, wav: &Path) -> Result, TrxError> { + let samples = + crate::audio::read_wav_mono_16k(wav).map_err(|e| TrxError::Load(e.to_string()))?; + self.run_full(&samples, 0, false) } } diff --git a/src/App.svelte b/src/App.svelte index 55aaf16..334c0e2 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -8,6 +8,7 @@ import ConsentNotice from "./lib/components/ConsentNotice.svelte"; import { recording } from "./lib/stores/recording.svelte"; import { settings } from "./lib/stores/settings.svelte"; + import { meetings } from "./lib/stores/meetings.svelte"; import { onMount } from "svelte"; let showSettings = $state(false); @@ -16,9 +17,11 @@ onMount(() => { recording.init(); settings.load(); + meetings.init(); }); function startRecording() { + meetings.deselect(); // show the live view, not whatever past meeting was open recording.start(undefined, settings.settings.default_record); } diff --git a/src/lib/api.ts b/src/lib/api.ts index 22b50a3..ea8c3aa 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -8,12 +8,14 @@ import { listen, type UnlistenFn } from "@tauri-apps/api/event"; export type MeetingId = string; export type BackendId = "npu" | "nvidia" | "amd" | "intel" | "cpu"; +export type MeetingStatus = "recording" | "transcribing" | "ready" | "recovering" | "error"; + export interface MeetingListItem { id: MeetingId; title: string; started_at: number; duration_secs: number | null; - status: "recording" | "transcribing" | "ready" | "recovering" | "error"; + status: MeetingStatus; } export interface TranscriptSegment { @@ -26,6 +28,29 @@ export interface TranscriptSegment { interim: boolean; } +export interface SpeakerInfo { + label: string; + display_name: string | null; + participant_id: string | null; +} + +// Full meeting detail (get_meeting) — DB row + transcript + speakers + notes. +export interface Meeting { + id: MeetingId; + title: string; + started_at: number; + ended_at: number | null; + duration_secs: number | null; + status: MeetingStatus; + recorded: boolean; + language: string | null; + backend_used: string | null; + model_used: string | null; + segments: TranscriptSegment[]; + speakers: SpeakerInfo[]; + notes_markdown: string; +} + export type SyncKind = "webdav" | "onedrive" | "dropbox" | "box"; // What the UI sees about a target — NEVER includes the secret (FR-SYNC-6). @@ -74,6 +99,8 @@ export interface AppSettings { consent_acknowledged: boolean; sync_enabled: boolean; mcp_enabled: boolean; + retention_max_age_days: number | null; + retention_max_size_gb: number | null; } // Feature brief — agent-ready spec distilled from a meeting (ADR-0011). @@ -116,12 +143,15 @@ export const api = { acknowledgeRecordingConsent: () => invoke("acknowledge_recording_consent"), hardwareStatus: () => invoke("hardware_status"), + resumeTranscription: (meetingId: MeetingId) => + invoke("resume_transcription", { meetingId }), listMeetings: (query?: string) => invoke("list_meetings", { query }), - getMeeting: (meetingId: MeetingId) => invoke("get_meeting", { meetingId }), + getMeeting: (meetingId: MeetingId) => invoke("get_meeting", { meetingId }), deleteMeeting: (meetingId: MeetingId) => invoke("delete_meeting", { meetingId }), updateNotes: (meetingId: MeetingId, markdown: string) => invoke("update_notes", { meetingId, markdown }), - exportMeeting: (meetingId: MeetingId, dest: string, format: string) => + // format ∈ "md" | "bundle"; dest is a file path for md, a folder for bundle. + exportMeeting: (meetingId: MeetingId, dest: string, format: "md" | "bundle") => invoke("export_meeting", { meetingId, dest, format }), llmStatus: () => invoke("llm_status"), @@ -194,6 +224,9 @@ export const events = { onSegment: ( cb: (p: { meetingId: string; segment: TranscriptSegment }) => void, ): Promise => listen("transcript://segment", (e) => cb(e.payload as never)), + onFinalized: ( + cb: (p: { meetingId: string; segmentCount: number }) => void, + ): Promise => listen("transcript://finalized", (e) => cb(e.payload as never)), onLlmToken: (cb: (p: { meetingId: string; text: string }) => void): Promise => listen("llm://token", (e) => cb(e.payload as never)), onSyncJob: ( diff --git a/src/lib/stores/meetings.svelte.ts b/src/lib/stores/meetings.svelte.ts new file mode 100644 index 0000000..9ff0f23 --- /dev/null +++ b/src/lib/stores/meetings.svelte.ts @@ -0,0 +1,61 @@ +// Meetings list + selected-meeting detail (Phase 2, FR-STORE-5). +// Svelte 5 runes store, same shape as recording.svelte.ts. + +import { api, events, type Meeting, type MeetingId, type MeetingListItem } from "../api"; + +class MeetingsStore { + list = $state([]); + selectedId = $state(null); + selected = $state(null); + loading = $state(false); + + async init() { + await this.load(); + // A meeting finishing (live stop, or a recovered meeting resuming + // transcription) changes the list and, if it's the open one, its detail. + await events.onFinalized(async ({ meetingId }) => { + await this.load(); + if (this.selectedId === meetingId) await this.select(meetingId); + }); + } + + async load(query?: string) { + this.loading = true; + try { + this.list = await api.listMeetings(query); + } catch { + // list_meetings backend not reachable; leave the list as-is. + } finally { + this.loading = false; + } + } + + async select(id: MeetingId) { + this.selectedId = id; + this.selected = await api.getMeeting(id); + } + + deselect() { + this.selectedId = null; + this.selected = null; + } + + async remove(id: MeetingId) { + await api.deleteMeeting(id); + if (this.selectedId === id) this.deselect(); + await this.load(); + } + + async resume(id: MeetingId) { + await api.resumeTranscription(id); + await this.load(); + if (this.selectedId === id) await this.select(id); + } + + async saveNotes(id: MeetingId, markdown: string) { + await api.updateNotes(id, markdown); + if (this.selected && this.selectedId === id) this.selected.notes_markdown = markdown; + } +} + +export const meetings = new MeetingsStore(); diff --git a/src/lib/stores/settings.svelte.ts b/src/lib/stores/settings.svelte.ts index 2cf0ff4..fec417d 100644 --- a/src/lib/stores/settings.svelte.ts +++ b/src/lib/stores/settings.svelte.ts @@ -17,6 +17,8 @@ const DEFAULT_SETTINGS: AppSettings = { consent_acknowledged: false, sync_enabled: false, // sync OFF by default (ADR-0010) mcp_enabled: false, // MCP server OFF by default (ADR-0011) + retention_max_age_days: null, // no cap by default (FR-STORE-2) + retention_max_size_gb: null, }; class SettingsStore { @@ -29,12 +31,17 @@ class SettingsStore { async load() { try { this.settings = await api.getSettings(); - this.targets = await api.listSyncTargets(); } catch { // Backend command is still a todo!(); fall back to local defaults. this.backendStub = true; this.settings = { ...DEFAULT_SETTINGS }; } + try { + this.targets = await api.listSyncTargets(); + } catch { + // Sync lands in Phase 9 — don't let its stub discard real settings above. + this.backendStub = true; + } this.loaded = true; } @@ -52,6 +59,10 @@ class SettingsStore { return this.patch({ default_record: on }); } + setRetentionPolicy(maxAgeDays: number | null, maxSizeGb: number | null) { + return this.patch({ retention_max_age_days: maxAgeDays, retention_max_size_gb: maxSizeGb }); + } + async acknowledgeConsent() { this.settings = { ...this.settings, consent_acknowledged: true }; try { diff --git a/src/lib/views/MeetingsList.svelte b/src/lib/views/MeetingsList.svelte index e5da7e2..d2d2402 100644 --- a/src/lib/views/MeetingsList.svelte +++ b/src/lib/views/MeetingsList.svelte @@ -1,37 +1,177 @@
- - {#if meetings.length === 0} + + {#if meetings.list.length === 0}

No meetings yet. Click ● Record to start.

{:else}
    - {#each meetings as m (m.id)} -
  • {m.title}
  • + {#each meetings.list as m (m.id)} +
  • + +
    + {#if m.status === "recovering"} + + {/if} + +
    +
  • {/each}
{/if}
diff --git a/src/lib/views/Settings.svelte b/src/lib/views/Settings.svelte index 8296e08..665dd4c 100644 --- a/src/lib/views/Settings.svelte +++ b/src/lib/views/Settings.svelte @@ -9,7 +9,7 @@ let { onClose }: { onClose: () => void } = $props(); - let section = $state<"recording" | "sync">("recording"); + let section = $state<"recording" | "storage" | "sync">("recording"); let showConsent = $state(false); let testResult = $state(null); @@ -31,6 +31,19 @@ showConsent = false; } + // ---- Storage retention (FR-STORE-2) ---- + let maxAgeInput = $state(""); + let maxSizeInput = $state(""); + $effect(() => { + maxAgeInput = settings.settings.retention_max_age_days?.toString() ?? ""; + maxSizeInput = settings.settings.retention_max_size_gb?.toString() ?? ""; + }); + function saveRetention() { + const age = maxAgeInput.trim() === "" ? null : Math.max(1, parseInt(maxAgeInput, 10) || 0); + const size = maxSizeInput.trim() === "" ? null : Math.max(1, parseInt(maxSizeInput, 10) || 0); + settings.setRetentionPolicy(age, size); + } + // ---- Add-target form ---- const PROVIDERS = ["nextcloud", "owncloud", "cloudreve", "seafile", "synology", "generic"]; let form = $state(blankForm("webdav")); @@ -80,15 +93,18 @@ + - {#if settings.backendStub} + {#if settings.backendStub && section === "sync"}

- Preview mode — the backend for these settings isn't implemented yet (Phase 9). Changes are - kept in the UI only. + Preview mode — the sync backend isn't implemented yet (Phase 9). Changes are kept in the UI + only.

{/if} @@ -117,6 +133,40 @@ (showConsent = false)} /> {/if} + {:else if section === "storage"} +
+

Storage

+
Location{settings.settings.storage_root}
+

+ Changing the storage location isn't supported yet — this is where meetings, models, and + the database currently live. +

+

Retention

+

+ Automatically delete the oldest meetings past a limit. Checked once at startup; empty + means no limit. Never touches a meeting that's currently recording. +

+
+ + +
+
{:else}

Sync & upload

diff --git a/src/lib/views/TranscriptNotes.svelte b/src/lib/views/TranscriptNotes.svelte index 5487c52..2ebc013 100644 --- a/src/lib/views/TranscriptNotes.svelte +++ b/src/lib/views/TranscriptNotes.svelte @@ -1,27 +1,230 @@
- {#if recording.segments.length === 0} -

Transcript will appear here as you record.

+ {#if meetings.selected} + {@const m = meetings.selected} +
+
+

Transcript

+ {#if m.segments.length === 0} +

No transcript for this meeting.

+ {:else} + {#each m.segments as s (s.id)} +

{speakerName(s.speaker, m.speakers)}: {s.text}

+ {/each} + {/if} +
+
+ +
+ + +
{@html renderNotes(notesText)}
+
+
+
+ {:else if recording.segments.length === 0} +

Transcript will appear here as you record.

{:else} - {#each recording.segments as s (s.id)} -

- {speakerName(s.speaker)}: {s.text} -

- {/each} +
+ {#each recording.segments as s (s.id)} +

+ {speakerName(s.speaker)}: + {s.text} +

+ {/each} +
{/if}