From e8e42eb2b942085f32dc6887183964aa2eee5965 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 20:30:47 +0000 Subject: [PATCH 01/20] feat(activation): license a demo song and name first-run next actions A clean launch now offers Try the demo and Use my own song. The bundled Late Night Set package is original CC0 audio with provenance hashes and enters through the same local-audio bootstrap as a user-selected file. Browser fallback fails closed. #828 still owns #770; do not mix with #811, #897, or #828. Canonical npm HIGH findings remain #783-owned. --- AGENTS.md | 1 + ARCHITECTURE.md | 1 + CHANGELOG.md | 1 + CLAUDE.md | 4 +- apps/desktop/core/src/lib.rs | 126 ++++++++++++++ apps/desktop/src-tauri/capabilities/main.json | 5 +- .../src-tauri/gen/schemas/acl-manifests.json | 2 +- .../src-tauri/gen/schemas/capabilities.json | 2 +- .../src-tauri/gen/schemas/desktop-schema.json | 12 ++ .../src-tauri/gen/schemas/linux-schema.json | 12 ++ .../src-tauri/gen/schemas/macOS-schema.json | 12 ++ .../src-tauri/gen/schemas/windows-schema.json | 12 ++ .../select_demo_audio_source.toml | 11 ++ apps/desktop/src-tauri/resources/demo/LICENSE | 12 ++ .../src-tauri/resources/demo/annotations.json | 37 +++++ .../resources/demo/late-night-set.wav | Bin 0 -> 88244 bytes .../src-tauri/resources/demo/provenance.json | 39 +++++ apps/desktop/src-tauri/src/main.rs | 30 ++++ apps/desktop/src-tauri/tauri.conf.json | 9 +- apps/desktop/src/App.test.tsx | 30 ++++ apps/desktop/src/App.tsx | 34 +++- .../workspace/WorkspaceStates.test.tsx | 63 +++++++ .../features/workspace/WorkspaceStates.tsx | 53 +++++- apps/desktop/src/lib/analysis.test.ts | 25 +++ apps/desktop/src/lib/analysis.ts | 31 +++- apps/desktop/src/lib/demo.test.ts | 125 ++++++++++++++ apps/desktop/src/lib/demo.ts | 155 ++++++++++++++++++ apps/desktop/src/locales/en/common.json | 12 +- apps/desktop/src/locales/ko/common.json | 12 +- apps/desktop/vite.config.ts | 2 + docs/activation/licensed-demo.md | 50 ++++++ scripts/generate_licensed_demo_wav.py | 51 ++++++ 32 files changed, 953 insertions(+), 18 deletions(-) create mode 100644 apps/desktop/src-tauri/permissions/autogenerated/select_demo_audio_source.toml create mode 100644 apps/desktop/src-tauri/resources/demo/LICENSE create mode 100644 apps/desktop/src-tauri/resources/demo/annotations.json create mode 100644 apps/desktop/src-tauri/resources/demo/late-night-set.wav create mode 100644 apps/desktop/src-tauri/resources/demo/provenance.json create mode 100644 apps/desktop/src/features/workspace/WorkspaceStates.test.tsx create mode 100644 apps/desktop/src/lib/demo.test.ts create mode 100644 apps/desktop/src/lib/demo.ts create mode 100644 docs/activation/licensed-demo.md create mode 100644 scripts/generate_licensed_demo_wav.py diff --git a/AGENTS.md b/AGENTS.md index fca448ce9..b0040d77a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -98,3 +98,4 @@ This section applies to any agent (Claude, Codex, Cursor, opencode, ...) working - Do not justify risky debug defaults as temporary shortcuts. - Do not defer dependency review, SBOM generation, or supply-chain checks to a later phase. - Do not defer Windows or macOS build enforcement to a later phase. +- The licensed demo is original CC0 audio that must pass the same local-audio intake as a user-selected file. Never present mocked browser success as production analysis. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3302a6fc3..8405fb997 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -113,6 +113,7 @@ Last updated: 2026-03-11 - The current shared-types baseline includes a rehearsal-domain fixture that exercises section, role, cue, confidence, provenance, and export-summary fields in the desktop shell before the full analysis pipeline lands. - Local analysis orchestration uses typed Tauri IPC commands and a Python subprocess over stdin/stdout rather than a loopback HTTP listener. - Local audio intake bootstraps a project by validating a user-selected file in Rust, creating app-owned temp/cache/project roots, and referencing the original source file rather than copying it in this phase. +- First-run empty workspace names **Try the demo** and **Use my own song**. The licensed demo is the CC0 `Late Night Set` package under `apps/desktop/src-tauri/resources/demo/` and enters through `select_demo_audio_source`, which reuses the same bootstrap as a user-selected local file. - Those bootstrap roots should resolve from app-owned Tauri data/cache paths instead of the shared system temp namespace. - Product and UX decisions should prefer rehearsal-first simplicity while still maintaining high analytical accuracy. - Security decisions should prefer allowlisted narrow capabilities over generic convenience APIs. diff --git a/CHANGELOG.md b/CHANGELOG.md index eea696893..8a3163349 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- First-run empty workspace offers **Try the demo** and **Use my own song**, with a CC0 licensed `Late Night Set` package that uses the same local-audio intake as a user-selected file (#964). - Display the analyzed song tempo (BPM) as a badge in the rehearsal workspace. - 각 합주 역할(Role)별 개인 연습 진행도를 0~100% 범위로 기록 및 시각화할 수 있는 연습 진척도(`practiceProgress`) 트래커 기능 추가. UI 컨트롤(슬라이더 및 +/- 버튼)과 한/영 다국어 지원 포함. diff --git a/CLAUDE.md b/CLAUDE.md index 82c2c704a..70c8dd773 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,8 +51,8 @@ BandScope is a local-first desktop app for rehearsal prep: it turns a song into Three layers, decoupled through shared contracts: -- `apps/desktop` — Tauri 2 + Vite + React 19 shell (Tailwind 4, Base UI, Storybook). Feature screens live in `src/features/` (home, workspace, chords, ranges, player, settings). `src/lib/analysis.ts` and `src/lib/job_runner.ts` call typed Tauri IPC commands, with a browser fallback that serves demo data when not running inside Tauri. -- `apps/desktop/src-tauri/src/main.rs` — the Rust orchestration boundary. Tauri commands (`start_analysis_job`, `get_analysis_job_status`, `select_local_audio_source`, `import_youtube_url`) validate untrusted input (project IDs, file paths, URLs) and spawn the Python engine as a subprocess. There is no loopback HTTP listener and no network path for local analysis. +- `apps/desktop` — Tauri 2 + Vite + React 19 shell (Tailwind 4, Base UI, Storybook). Feature screens live in `src/features/` (home, workspace, chords, ranges, player, settings). `src/lib/analysis.ts` and `src/lib/job_runner.ts` call typed Tauri IPC commands, with a browser fallback that serves demo analysis data when not running inside Tauri. Bundled licensed-demo intake is desktop-only and fails closed in the browser. +- `apps/desktop/src-tauri/src/main.rs` — the Rust orchestration boundary. Tauri commands (`start_analysis_job`, `get_analysis_job_status`, `select_local_audio_source`, `select_demo_audio_source`, `import_youtube_url`) validate untrusted input (project IDs, file paths, URLs) and spawn the Python engine as a subprocess. There is no loopback HTTP listener and no network path for local analysis. - `services/analysis-engine` — Python package `bandscope_analysis` (librosa/numpy). Entry point `cli.py` reads a JSON job request on stdin and prints a structured job-status JSON envelope on stdout (`--progress-jsonl` streams progress lines). `api.py` orchestrates the pipeline across the `separation`, `sections`, `roles`, `chords`, `ranges`, `temporal`, `transcription`, and `youtube` modules. Data flow: React UI → Tauri IPC command → Rust validation + Python subprocess over stdin/stdout → job status and progress events emitted back to the UI. diff --git a/apps/desktop/core/src/lib.rs b/apps/desktop/core/src/lib.rs index 200726570..1f8273215 100644 --- a/apps/desktop/core/src/lib.rs +++ b/apps/desktop/core/src/lib.rs @@ -40,6 +40,17 @@ pub const ANALYSIS_WAIT_POLL: Duration = Duration::from_millis(50); pub const AUDIO_EXTENSIONS: [&str; 4] = ["wav", "mp3", "flac", "m4a"]; +pub const DEMO_AUDIO_FILE_NAME: &str = "late-night-set.wav"; + +pub const DEMO_AUDIO_BYTES: u64 = 88244; + +pub const DEMO_UNAVAILABLE_MESSAGE: &str = + "The licensed demo song could not be loaded. Use your own song to start tonight."; + +const WAV_RIFF_MAGIC: &[u8] = b"RIFF"; + +const WAV_WAVE_MAGIC: &[u8] = b"WAVE"; + pub const MISSING_ANALYSIS_PYTHON: &str = "__bandscope_missing_analysis_python__"; pub const YOUTUBE_IMPORT_TIMEOUT: Duration = Duration::from_secs(120); @@ -659,6 +670,60 @@ pub fn validate_score_pdf_source(path: &Path) -> Result<(PathBuf, String, u64), /// WebView. The path is rebuilt server-side from validated ids, symlinks are /// refused, and the canonicalized result must still live under the /// canonicalized app-owned scores root (path-traversal guard). + +/// Validate the bundled licensed demo WAV before it reuses local-audio bootstrap. +pub fn validate_demo_audio_source(path: &Path) -> Result { + let link_metadata = std::fs::symlink_metadata(path) + .map_err(|_| DEMO_UNAVAILABLE_MESSAGE.to_string())?; + #[cfg(not(all(coverage, windows)))] + if link_metadata.file_type().is_symlink() { + return Err(DEMO_UNAVAILABLE_MESSAGE.to_string()); + } + + #[cfg(coverage)] + let canonical = path + .canonicalize() + .expect("demo audio path should canonicalize after metadata lookup"); + #[cfg(not(coverage))] + let canonical = path + .canonicalize() + .map_err(|_| DEMO_UNAVAILABLE_MESSAGE.to_string())?; + + let file_name = canonical + .file_name() + .and_then(|value| value.to_str()) + .ok_or_else(|| DEMO_UNAVAILABLE_MESSAGE.to_string())?; + if file_name != DEMO_AUDIO_FILE_NAME { + return Err(DEMO_UNAVAILABLE_MESSAGE.to_string()); + } + let extension = canonical + .extension() + .and_then(|value| value.to_str()) + .map(|value| value.to_ascii_lowercase()) + .ok_or_else(|| DEMO_UNAVAILABLE_MESSAGE.to_string())?; + if extension != "wav" { + return Err(DEMO_UNAVAILABLE_MESSAGE.to_string()); + } + if !link_metadata.is_file() || link_metadata.len() != DEMO_AUDIO_BYTES { + return Err(DEMO_UNAVAILABLE_MESSAGE.to_string()); + } + + let mut header = [0u8; 12]; + std::fs::File::open(&canonical) + .and_then(|mut file| file.read_exact(&mut header)) + .map_err(|_| DEMO_UNAVAILABLE_MESSAGE.to_string())?; + if &header[0..4] != WAV_RIFF_MAGIC || &header[8..12] != WAV_WAVE_MAGIC { + return Err(DEMO_UNAVAILABLE_MESSAGE.to_string()); + } + + Ok(LocalAudioSourcePayload { + source_path: canonical.to_string_lossy().into_owned(), + file_name: file_name.to_string(), + extension, + file_size_bytes: link_metadata.len(), + }) +} + pub fn resolve_existing_score_pdf(scores_root: &Path, score_id: &str) -> Result { if !is_valid_score_id(score_id) { return Err("Score was not found.".to_string()); @@ -1280,4 +1345,65 @@ mod tests { let _ = std::fs::remove_dir_all(scores_root); let _ = std::fs::remove_dir_all(outside_root); } + + fn write_sized_demo_wav(path: &Path, bytes: u64, riff: bool, wave: bool) { + let mut data = vec![0u8; bytes as usize]; + if riff && data.len() >= 4 { + data[0..4].copy_from_slice(b"RIFF"); + } + if wave && data.len() >= 12 { + data[8..12].copy_from_slice(b"WAVE"); + } + std::fs::write(path, data).expect("demo fixture should be written"); + } + + #[test] + fn demo_audio_validation_accepts_the_licensed_wav_contract() { + let root = unique_test_dir("demo-valid"); + std::fs::create_dir_all(&root).expect("demo root should be created"); + let path = root.join(DEMO_AUDIO_FILE_NAME); + write_sized_demo_wav(&path, DEMO_AUDIO_BYTES, true, true); + let source = validate_demo_audio_source(&path).expect("licensed demo wav should validate"); + assert_eq!(source.file_name, DEMO_AUDIO_FILE_NAME); + assert_eq!(source.extension, "wav"); + assert_eq!(source.file_size_bytes, DEMO_AUDIO_BYTES); + let _ = std::fs::remove_dir_all(root); + } + + #[test] + fn demo_audio_validation_rejects_wrong_name_size_magic_and_missing_files() { + let root = unique_test_dir("demo-reject"); + std::fs::create_dir_all(&root).expect("demo root should be created"); + + let wrong_name = root.join("other.wav"); + write_sized_demo_wav(&wrong_name, DEMO_AUDIO_BYTES, true, true); + assert!(validate_demo_audio_source(&wrong_name).is_err()); + + let wrong_size = root.join(DEMO_AUDIO_FILE_NAME); + write_sized_demo_wav(&wrong_size, 12, true, true); + assert!(validate_demo_audio_source(&wrong_size).is_err()); + + write_sized_demo_wav(&wrong_size, DEMO_AUDIO_BYTES, false, true); + assert!(validate_demo_audio_source(&wrong_size).is_err()); + + write_sized_demo_wav(&wrong_size, DEMO_AUDIO_BYTES, true, false); + assert!(validate_demo_audio_source(&wrong_size).is_err()); + + let missing = root.join("missing").join(DEMO_AUDIO_FILE_NAME); + assert!(validate_demo_audio_source(&missing).is_err()); + + #[cfg(unix)] + { + let valid = root.join("target.wav"); + write_sized_demo_wav(&valid, DEMO_AUDIO_BYTES, true, true); + let linked = root.join(DEMO_AUDIO_FILE_NAME); + let _ = std::fs::remove_file(&linked); + std::os::unix::fs::symlink(&valid, &linked).expect("symlink should be created"); + assert!(validate_demo_audio_source(&linked).is_err()); + } + + let _ = std::fs::remove_dir_all(root); + } + + } diff --git a/apps/desktop/src-tauri/capabilities/main.json b/apps/desktop/src-tauri/capabilities/main.json index 8103420f4..bbc4c0ed9 100644 --- a/apps/desktop/src-tauri/capabilities/main.json +++ b/apps/desktop/src-tauri/capabilities/main.json @@ -2,13 +2,16 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "main-capability", "description": "Capability for the main BandScope window to use the analysis orchestration commands.", - "windows": ["main"], + "windows": [ + "main" + ], "permissions": [ "core:event:allow-listen", "core:event:allow-unlisten", "allow-start-analysis-job", "allow-get-analysis-job-status", "allow-select-local-audio-source", + "allow-select-demo-audio-source", "allow-import-youtube-url", "allow-save-project", "allow-load-project", diff --git a/apps/desktop/src-tauri/gen/schemas/acl-manifests.json b/apps/desktop/src-tauri/gen/schemas/acl-manifests.json index c5cda5d6e..56d608fdc 100644 --- a/apps/desktop/src-tauri/gen/schemas/acl-manifests.json +++ b/apps/desktop/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"__app-acl__":{"default_permission":null,"permissions":{"allow-attach-score-pdf":{"identifier":"allow-attach-score-pdf","description":"Enables the attach_score_pdf command without any pre-configured scope.","commands":{"allow":["attach_score_pdf"],"deny":[]}},"allow-get-analysis-job-status":{"identifier":"allow-get-analysis-job-status","description":"Enables the get_analysis_job_status command without any pre-configured scope.","commands":{"allow":["get_analysis_job_status"],"deny":[]}},"allow-import-youtube-url":{"identifier":"allow-import-youtube-url","description":"Enables the import_youtube_url command without any pre-configured scope.","commands":{"allow":["import_youtube_url"],"deny":[]}},"allow-load-project":{"identifier":"allow-load-project","description":"Enables the load_project command without any pre-configured scope.","commands":{"allow":["load_project"],"deny":[]}},"allow-read-score-pdf":{"identifier":"allow-read-score-pdf","description":"Enables the read_score_pdf command without any pre-configured scope.","commands":{"allow":["read_score_pdf"],"deny":[]}},"allow-remove-score-pdf":{"identifier":"allow-remove-score-pdf","description":"Enables the remove_score_pdf command without any pre-configured scope.","commands":{"allow":["remove_score_pdf"],"deny":[]}},"allow-save-project":{"identifier":"allow-save-project","description":"Enables the save_project command without any pre-configured scope.","commands":{"allow":["save_project"],"deny":[]}},"allow-select-local-audio-source":{"identifier":"allow-select-local-audio-source","description":"Enables the select_local_audio_source command without any pre-configured scope.","commands":{"allow":["select_local_audio_source"],"deny":[]}},"allow-start-analysis-job":{"identifier":"allow-start-analysis-job","description":"Enables the start_analysis_job command without any pre-configured scope.","commands":{"allow":["start_analysis_job"],"deny":[]}},"deny-attach-score-pdf":{"identifier":"deny-attach-score-pdf","description":"Denies the attach_score_pdf command without any pre-configured scope.","commands":{"allow":[],"deny":["attach_score_pdf"]}},"deny-get-analysis-job-status":{"identifier":"deny-get-analysis-job-status","description":"Denies the get_analysis_job_status command without any pre-configured scope.","commands":{"allow":[],"deny":["get_analysis_job_status"]}},"deny-import-youtube-url":{"identifier":"deny-import-youtube-url","description":"Denies the import_youtube_url command without any pre-configured scope.","commands":{"allow":[],"deny":["import_youtube_url"]}},"deny-load-project":{"identifier":"deny-load-project","description":"Denies the load_project command without any pre-configured scope.","commands":{"allow":[],"deny":["load_project"]}},"deny-read-score-pdf":{"identifier":"deny-read-score-pdf","description":"Denies the read_score_pdf command without any pre-configured scope.","commands":{"allow":[],"deny":["read_score_pdf"]}},"deny-remove-score-pdf":{"identifier":"deny-remove-score-pdf","description":"Denies the remove_score_pdf command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_score_pdf"]}},"deny-save-project":{"identifier":"deny-save-project","description":"Denies the save_project command without any pre-configured scope.","commands":{"allow":[],"deny":["save_project"]}},"deny-select-local-audio-source":{"identifier":"deny-select-local-audio-source","description":"Denies the select_local_audio_source command without any pre-configured scope.","commands":{"allow":[],"deny":["select_local_audio_source"]}},"deny-start-analysis-job":{"identifier":"deny-start-analysis-job","description":"Denies the start_analysis_job command without any pre-configured scope.","commands":{"allow":[],"deny":["start_analysis_job"]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","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 +{"__app-acl__":{"default_permission":null,"permissions":{"allow-attach-score-pdf":{"identifier":"allow-attach-score-pdf","description":"Enables the attach_score_pdf command without any pre-configured scope.","commands":{"allow":["attach_score_pdf"],"deny":[]}},"allow-get-analysis-job-status":{"identifier":"allow-get-analysis-job-status","description":"Enables the get_analysis_job_status command without any pre-configured scope.","commands":{"allow":["get_analysis_job_status"],"deny":[]}},"allow-import-youtube-url":{"identifier":"allow-import-youtube-url","description":"Enables the import_youtube_url command without any pre-configured scope.","commands":{"allow":["import_youtube_url"],"deny":[]}},"allow-load-project":{"identifier":"allow-load-project","description":"Enables the load_project command without any pre-configured scope.","commands":{"allow":["load_project"],"deny":[]}},"allow-read-score-pdf":{"identifier":"allow-read-score-pdf","description":"Enables the read_score_pdf command without any pre-configured scope.","commands":{"allow":["read_score_pdf"],"deny":[]}},"allow-remove-score-pdf":{"identifier":"allow-remove-score-pdf","description":"Enables the remove_score_pdf command without any pre-configured scope.","commands":{"allow":["remove_score_pdf"],"deny":[]}},"allow-save-project":{"identifier":"allow-save-project","description":"Enables the save_project command without any pre-configured scope.","commands":{"allow":["save_project"],"deny":[]}},"allow-select-local-audio-source":{"identifier":"allow-select-local-audio-source","description":"Enables the select_local_audio_source command without any pre-configured scope.","commands":{"allow":["select_local_audio_source"],"deny":[]}},"allow-start-analysis-job":{"identifier":"allow-start-analysis-job","description":"Enables the start_analysis_job command without any pre-configured scope.","commands":{"allow":["start_analysis_job"],"deny":[]}},"deny-attach-score-pdf":{"identifier":"deny-attach-score-pdf","description":"Denies the attach_score_pdf command without any pre-configured scope.","commands":{"allow":[],"deny":["attach_score_pdf"]}},"deny-get-analysis-job-status":{"identifier":"deny-get-analysis-job-status","description":"Denies the get_analysis_job_status command without any pre-configured scope.","commands":{"allow":[],"deny":["get_analysis_job_status"]}},"deny-import-youtube-url":{"identifier":"deny-import-youtube-url","description":"Denies the import_youtube_url command without any pre-configured scope.","commands":{"allow":[],"deny":["import_youtube_url"]}},"deny-load-project":{"identifier":"deny-load-project","description":"Denies the load_project command without any pre-configured scope.","commands":{"allow":[],"deny":["load_project"]}},"deny-read-score-pdf":{"identifier":"deny-read-score-pdf","description":"Denies the read_score_pdf command without any pre-configured scope.","commands":{"allow":[],"deny":["read_score_pdf"]}},"deny-remove-score-pdf":{"identifier":"deny-remove-score-pdf","description":"Denies the remove_score_pdf command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_score_pdf"]}},"deny-save-project":{"identifier":"deny-save-project","description":"Denies the save_project command without any pre-configured scope.","commands":{"allow":[],"deny":["save_project"]}},"deny-select-local-audio-source":{"identifier":"deny-select-local-audio-source","description":"Denies the select_local_audio_source command without any pre-configured scope.","commands":{"allow":[],"deny":["select_local_audio_source"]}},"deny-start-analysis-job":{"identifier":"deny-start-analysis-job","description":"Denies the start_analysis_job command without any pre-configured scope.","commands":{"allow":[],"deny":["start_analysis_job"]}},"allow-select-demo-audio-source":{"identifier":"allow-select-demo-audio-source","description":"Enables the select_demo_audio_source command without any pre-configured scope.","commands":{"allow":["select_demo_audio_source"],"deny":[]}},"deny-select-demo-audio-source":{"identifier":"deny-select-demo-audio-source","description":"Denies the select_demo_audio_source command without any pre-configured scope.","commands":{"allow":[],"deny":["select_demo_audio_source"]}}},"permission_sets":{},"global_scope_schema":null},"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener","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}} diff --git a/apps/desktop/src-tauri/gen/schemas/capabilities.json b/apps/desktop/src-tauri/gen/schemas/capabilities.json index 25d764245..78472ae56 100644 --- a/apps/desktop/src-tauri/gen/schemas/capabilities.json +++ b/apps/desktop/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"main-capability":{"identifier":"main-capability","description":"Capability for the main BandScope window to use the analysis orchestration commands.","local":true,"windows":["main"],"permissions":["core:event:allow-listen","core:event:allow-unlisten","allow-start-analysis-job","allow-get-analysis-job-status","allow-select-local-audio-source","allow-import-youtube-url","allow-save-project","allow-load-project","allow-attach-score-pdf","allow-read-score-pdf","allow-remove-score-pdf"]}} \ No newline at end of file +{"main-capability":{"identifier":"main-capability","description":"Capability for the main BandScope window to use the analysis orchestration commands.","local":true,"windows":["main"],"permissions":["core:event:allow-listen","core:event:allow-unlisten","allow-start-analysis-job","allow-get-analysis-job-status","allow-select-local-audio-source","allow-select-demo-audio-source","allow-import-youtube-url","allow-save-project","allow-load-project","allow-attach-score-pdf","allow-read-score-pdf","allow-remove-score-pdf"]}} diff --git a/apps/desktop/src-tauri/gen/schemas/desktop-schema.json b/apps/desktop/src-tauri/gen/schemas/desktop-schema.json index a46e367ae..bea6d5794 100644 --- a/apps/desktop/src-tauri/gen/schemas/desktop-schema.json +++ b/apps/desktop/src-tauri/gen/schemas/desktop-schema.json @@ -224,6 +224,12 @@ "const": "allow-select-local-audio-source", "markdownDescription": "Enables the select_local_audio_source command without any pre-configured scope." }, + { + "description": "Enables the select_demo_audio_source command without any pre-configured scope.", + "type": "string", + "const": "allow-select-demo-audio-source", + "markdownDescription": "Enables the select_demo_audio_source command without any pre-configured scope." + }, { "description": "Enables the start_analysis_job command without any pre-configured scope.", "type": "string", @@ -278,6 +284,12 @@ "const": "deny-select-local-audio-source", "markdownDescription": "Denies the select_local_audio_source command without any pre-configured scope." }, + { + "description": "Denies the select_demo_audio_source command without any pre-configured scope.", + "type": "string", + "const": "deny-select-demo-audio-source", + "markdownDescription": "Denies the select_demo_audio_source command without any pre-configured scope." + }, { "description": "Denies the start_analysis_job command without any pre-configured scope.", "type": "string", diff --git a/apps/desktop/src-tauri/gen/schemas/linux-schema.json b/apps/desktop/src-tauri/gen/schemas/linux-schema.json index 1c57ee001..199240fe9 100644 --- a/apps/desktop/src-tauri/gen/schemas/linux-schema.json +++ b/apps/desktop/src-tauri/gen/schemas/linux-schema.json @@ -188,6 +188,12 @@ "const": "allow-select-local-audio-source", "markdownDescription": "Enables the select_local_audio_source command without any pre-configured scope." }, + { + "description": "Enables the select_demo_audio_source command without any pre-configured scope.", + "type": "string", + "const": "allow-select-demo-audio-source", + "markdownDescription": "Enables the select_demo_audio_source command without any pre-configured scope." + }, { "description": "Enables the start_analysis_job command without any pre-configured scope.", "type": "string", @@ -206,6 +212,12 @@ "const": "deny-select-local-audio-source", "markdownDescription": "Denies the select_local_audio_source command without any pre-configured scope." }, + { + "description": "Denies the select_demo_audio_source command without any pre-configured scope.", + "type": "string", + "const": "deny-select-demo-audio-source", + "markdownDescription": "Denies the select_demo_audio_source command without any pre-configured scope." + }, { "description": "Denies the start_analysis_job command without any pre-configured scope.", "type": "string", diff --git a/apps/desktop/src-tauri/gen/schemas/macOS-schema.json b/apps/desktop/src-tauri/gen/schemas/macOS-schema.json index a46e367ae..bea6d5794 100644 --- a/apps/desktop/src-tauri/gen/schemas/macOS-schema.json +++ b/apps/desktop/src-tauri/gen/schemas/macOS-schema.json @@ -224,6 +224,12 @@ "const": "allow-select-local-audio-source", "markdownDescription": "Enables the select_local_audio_source command without any pre-configured scope." }, + { + "description": "Enables the select_demo_audio_source command without any pre-configured scope.", + "type": "string", + "const": "allow-select-demo-audio-source", + "markdownDescription": "Enables the select_demo_audio_source command without any pre-configured scope." + }, { "description": "Enables the start_analysis_job command without any pre-configured scope.", "type": "string", @@ -278,6 +284,12 @@ "const": "deny-select-local-audio-source", "markdownDescription": "Denies the select_local_audio_source command without any pre-configured scope." }, + { + "description": "Denies the select_demo_audio_source command without any pre-configured scope.", + "type": "string", + "const": "deny-select-demo-audio-source", + "markdownDescription": "Denies the select_demo_audio_source command without any pre-configured scope." + }, { "description": "Denies the start_analysis_job command without any pre-configured scope.", "type": "string", diff --git a/apps/desktop/src-tauri/gen/schemas/windows-schema.json b/apps/desktop/src-tauri/gen/schemas/windows-schema.json index a46e367ae..bea6d5794 100644 --- a/apps/desktop/src-tauri/gen/schemas/windows-schema.json +++ b/apps/desktop/src-tauri/gen/schemas/windows-schema.json @@ -224,6 +224,12 @@ "const": "allow-select-local-audio-source", "markdownDescription": "Enables the select_local_audio_source command without any pre-configured scope." }, + { + "description": "Enables the select_demo_audio_source command without any pre-configured scope.", + "type": "string", + "const": "allow-select-demo-audio-source", + "markdownDescription": "Enables the select_demo_audio_source command without any pre-configured scope." + }, { "description": "Enables the start_analysis_job command without any pre-configured scope.", "type": "string", @@ -278,6 +284,12 @@ "const": "deny-select-local-audio-source", "markdownDescription": "Denies the select_local_audio_source command without any pre-configured scope." }, + { + "description": "Denies the select_demo_audio_source command without any pre-configured scope.", + "type": "string", + "const": "deny-select-demo-audio-source", + "markdownDescription": "Denies the select_demo_audio_source command without any pre-configured scope." + }, { "description": "Denies the start_analysis_job command without any pre-configured scope.", "type": "string", diff --git a/apps/desktop/src-tauri/permissions/autogenerated/select_demo_audio_source.toml b/apps/desktop/src-tauri/permissions/autogenerated/select_demo_audio_source.toml new file mode 100644 index 000000000..5c0b36e44 --- /dev/null +++ b/apps/desktop/src-tauri/permissions/autogenerated/select_demo_audio_source.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-select-demo-audio-source" +description = "Enables the select_demo_audio_source command without any pre-configured scope." +commands.allow = ["select_demo_audio_source"] + +[[permission]] +identifier = "deny-select-demo-audio-source" +description = "Denies the select_demo_audio_source command without any pre-configured scope." +commands.deny = ["select_demo_audio_source"] diff --git a/apps/desktop/src-tauri/resources/demo/LICENSE b/apps/desktop/src-tauri/resources/demo/LICENSE new file mode 100644 index 000000000..b91a8da51 --- /dev/null +++ b/apps/desktop/src-tauri/resources/demo/LICENSE @@ -0,0 +1,12 @@ +CC0 1.0 Universal + +Late Night Set is original audio created for BandScope evaluation by +Contextual Wisdom Lab. To the extent possible under law, the author has +waived all copyright and related or neighboring rights to this demo +package (audio, annotations, and this license text). + +You may copy, modify, redistribute, and perform the demo for product +evaluation and rehearsal. This is not a commercial recording and must +not be presented as one. + +Full legal text: https://creativecommons.org/publicdomain/zero/1.0/legalcode diff --git a/apps/desktop/src-tauri/resources/demo/annotations.json b/apps/desktop/src-tauri/resources/demo/annotations.json new file mode 100644 index 000000000..e945a8d2a --- /dev/null +++ b/apps/desktop/src-tauri/resources/demo/annotations.json @@ -0,0 +1,37 @@ +{ + "songId": "late-night-set", + "title": "Late Night Set", + "performer": "Contextual Wisdom Lab", + "sections": [ + { + "id": "verse-1", + "label": "verse", + "timeRange": { + "start": 0, + "end": 1 + }, + "notes": "Lower tone; treat as the first entrance." + }, + { + "id": "chorus-1", + "label": "chorus", + "timeRange": { + "start": 1, + "end": 2 + }, + "notes": "Higher tone; treat as the lift." + } + ], + "roles": [ + { + "id": "lead-vocal", + "name": "Lead Vocal", + "rehearsalPriority": "high" + }, + { + "id": "keys-right", + "name": "Keys Right", + "rehearsalPriority": "medium" + } + ] +} diff --git a/apps/desktop/src-tauri/resources/demo/late-night-set.wav b/apps/desktop/src-tauri/resources/demo/late-night-set.wav new file mode 100644 index 0000000000000000000000000000000000000000..f933f0a5e0fd4bc09ae489e2c18f3f8b29a3837a GIT binary patch literal 88244 zcmeI5=XX`rx`%h~eYH6$3TQZVDN>JgjR8ceGzAG_5~M{5ARqyPbkNWwK?DS)Nfp5u zr7Km+pmB1A86%(gkh#~Ib1#1D_dd_?0r~j{$H--)`;L08 zXnI)_n@lE?=YE=vlF42@D3d8oya5Y|sOK>ZE8x^7lTo-O3dziUQ-=NMqcG#!e zT3H{M=b8-04SKt7g(j*hR9=y{lWoo8#_a{Rdl_HP3R{G{Gq7r|ACH3|F64Hdm~b z1u|pO=aTIbyJC%^JHu^5mjW~VFZv|UIrjnAUT2N;NNgz16zceQ`6M|;7LzGt8d*tB z6Ei=DkMfm5Ur{gd(jU&hT`6}HvTMGm0QIWvWOfdaq1*dVu3Gm~Ph(%P z|4bk+v?WYMt7CbIn&gnQkg1S0Qk+l@P(Rd6)IHIUGM+U(XPIt2Wc$+|cPQyN{Ri_c zJC+M`^ASPa@khC$UV}A-^S+&7lm?h zu+-7n#?{?D&a>8g)7LbxB`+J89zoJAtyJOt75M)A|0TJ@)Q=UexF7PHEbztCey^k?i+Or}Va@AyFQSM;3-np}m2jey#6@XRmvMtJ1k! z`du`Ng~AQ~Ej~yNl7*y%Oebr}uY~27@G4=aFhXo3r6j*I;o>~6c|Y~t@wX3d4Ot_f zMcMebiSDWYq^D-p@*RpUsw?VI+B)4B!&PGk^J>dItJU7!F_aof_h%Zjf3xejW@rae z;y3XkyajK@^KgG0M(dCX&EhVwDdst*4V_0B9gl2ZTSr>rrb=TY!*(5~U8N2yCn$cE zwaQkeW6Adu4`Rcjcf#+6qJgjcLw&SYaNl%YaXyfeVrOxg;NfTRTE3PnBU8x-WGOj8 zV&jX?0z+%M6q~lP^>{)VnmTbqDn?7(X|8&3&vDwjby@aRX&vQFu6@C{t!e`Jx^b5ywW7*}*SM&~Qonwl4Hhz^Fo(=+$M)I(c8&-i#&v zagxNx#qLMm4m*P@10DR5_qb=Bd!B2)^Kr`|=mJ9rAcyLpjq6H<|MWww?4SH(D$TRl&k(akqF zjl<0QEdgt5`v^w~^#MJGY0vuEZQM&}KQiJGxFWaHH{#j2Cw8FFmC#+3NqhwI$>wF&rlN`B_@n6f$g|MA zAP!veE%T1>baFR$wRILq^TqQ*Ghr*=ioZiPlMiz{WG>lBo|4}DalWrWgk9onX@c`@ z*EDyf=c3o>pBOkF>=6Dg(k@mLADDcSnwN3N4$1SCb*gEai1uT>-!Q>+)=XJP+t%0* zIZjbE^bV$!ZN**Z#-MBHMf^Tqo7*94aS?8gucNooEv_9`#(u+`rq5G9I5yZP*cw_d zn~P0x!xDW~J6lt)8lpTYZzQY8h^Z0D^YJdR*M zC&WF8rKyIQli4A1QL#v6*LjS`hHWAw9d2zDgJFS;ByWnpUTouZM7e-a_jfsDyPN&CY1G2S>7gQ(JuWGOA zUN_Vlo0=C`E?VXGHjY=R0=hdxu@BkhTqCpCA2S`7d;T`nz))QOeZs&WiKf%Dc@F0nvZmTeUb5&sjcM`>(4gPuA!{7im7J~ zu@kuzw+Kn-6%U!G+nE+3`#%J%bwS=xC2yk6Ol>L&l{Bt-eAV zQBPJ~SG+9Wn$4z*Q+E>mXok#C7a3ivbp5FsL*5LZiO&RMSI?t`9xysi9;15bk^!q+3iVt>VFB-QEdnQpS% z@~KLhx>AF6U+NvkMW)B*4%RZ;9{VN7UFrsXoT*@YaCO{dbQiV8Q}AcG9kLo1;^* zmU=q7x$@n`o~_)>j**U>4|08hXxbL*(Y({X!Tho+%tTt4?XdyaWT6Y38~t$mfP zz#27Inc5gn=wH@-r)j8OuJp)9%4#!B(yNoP__UZ1nHEk3w*>n5W8Sl#t?p&6WzK5p zf~XM3377eyypQZBpX5&78RRo^i8SDs^G0E>@V3}o(mK;lt@}mK2=7MUKmIbDXS|8X{j_0Z7G-jgA8Mc(uqtz&g`r(grtJs1U;~`j$zeagzJ@<@l#uhMR z=}}Z~2W$V+y1~-je8)J^P_LVyy{+!8+NZF{%d&r@`=t&io{w*dqR6gLw_u%rt?vzQ zV~^Y&bt;@qrP1PUK`Sif5r2`a%Uz9<+*LV1{A4(PjUO*)#Y^ILX^nHOYp45?N9HT= z?+BQX&Wn4aGa`}Ds$k2&ecwiJp{I|#qpO>9xU^KfDzp@K@a_2f zq$;;NigUYUJ9$ic@Q3*x!Xu$toGOiQ4slI%FZKNBP5Fifz7IADS4SGf_Qbm;ucwMK zkeonr0?7#^Cy<;#astT-BqxxZKym`f2_z?woIr8{$q6JUkeonr0-Y1+oIvLUIw#OM zfzAnZPM~uFofGJsK<5NHC(t>8&Ixo*pmPG96PVV4X&so>foUC>)`4jqnAU-59hlaE zX&so>foUC>)`4jqnAU-59hlYu&Ixc%fO7(z6X2Wx=L9$>z&QcV32;t;a{`7<`%NOm>X0_$8=kTWv*$6!LB=_E>xy+ zjgt1MyYXewf#DXxX8vB@#cro_ne?VONGRt8vW66sa&m)==8<3(dq`h6o4D_Jj`@xR zeh(?4Z^ZW`^_f!HeMMLG8tqMe%4o63Y&Y#?l!xxl4&yqbztJeX9&f7t`D=y+*$^*daVMS`(*J^D<9l`O0t95p5U41k+T@Xxnp+Q&c;q zlr87Rpd@-f_ere91Mqc(Q9I7co~CLYW;koQWI1G;?oiUxm~Ytw+OeedDga3)I5)O(1 zso16W-1T1Y-w9^Iy<-~_fz+7n8TkvUa?Mqp!r0W@%qq9nItu9h%tQ7rw-vR(3vo4G zfM38H&{gglyOrrm?{&CrMyuW|7>?-P)96&Y<*#J#r{*T|V)fzL;6eXsZ`l2o>mO;m zxK2368~Gh%D*1?fPulR0_>00*v7_^-Yn-RGuW6u5=>5onm@HYCzMOqYu|XwiI_Zmy zi_M={N7^;iTDqPwa4D_^y@FTdK8u;yj6Xs`MQX@TKTg4 z_e^ohkf@C=437y8^_O_}x-G8VQi)h7e8vaKdQw8>kzYvxuM#3cBdN@pa2@n~>RTMx z8oC}~<1>=~Nq3X&P{`Dyv|sA48XueQS@+n7Qa9+vY!9v(x{Kb#pW)585QotaG>eBQ5!+Mus}=Ds?O6ud?x(Sn{j*uqX;g17RQS9qYd0ERs5lU4$9@Uu0SC z-dsu^k|Ms1&_Wz7)j0FrS=*Pi9e0SD(`=3~fz4Ep2R3do^WY zMzST`>&Sy9;ElKv565+=HOl8&vybRWRIS} zkbI%CPTg8J#qhbQ+EQW5cRZm6GppH6+!Un6)9@C&3irWhkOr|_fcc6>)D-&$Yn6GC zaj;&kJ)|0>sLw7;H%k5%TND`*>K5qdo8h_O>gV)}w*{W>#Q#Xj$Oq(0qUQJU6+)$W zTk7xn$GzXX*}p4zHEf8zlc-HK$*z#Qlm(j2x|;^SDQtOQ+vI3YuVK!xXSmg}P_l&J!#?sdvti8LnySbsUURSN@t*VokWi9E$iE%L$aR%%BlDDyEo=f5U zN!%@*=Mle$%pfz#0rEV5jXxq>5*^O9t_~iVFW^^&Iz(p2t|VHeE3;AgIMolDjP7OQ zF!K;=Yx@(&2lPe8&kEdr)Dc(Y{)*Wc;RzgQXVGd zVNxC@EfFH1Jx2*Al#z;I#y=C0L2UN(@$Fuo8on7_7wLln19gIOV}94^DY- z%8$bW>V?L0ud^9?1@+X<+JI68lq#T90i_BkRY0i%N)=G5fKmmNDxg#Wr3xrjK&b*s z6;P^xQU#PMpi}{+3Mf@TsRBwBP^y4Z1(YhFQ~{+5C{;kI0!kH7s(?}jlq#T90i_Bk ORY0i%O4a|BQuV)~uVFm^ literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/resources/demo/provenance.json b/apps/desktop/src-tauri/resources/demo/provenance.json new file mode 100644 index 000000000..307e49d06 --- /dev/null +++ b/apps/desktop/src-tauri/resources/demo/provenance.json @@ -0,0 +1,39 @@ +{ + "manifestVersion": 1, + "artifactKind": "bandscope.licensed-demo", + "song": { + "id": "late-night-set", + "title": "Late Night Set", + "performer": "Contextual Wisdom Lab", + "license": "CC0-1.0", + "licenseUrl": "https://creativecommons.org/publicdomain/zero/1.0/", + "permittedUses": [ + "evaluation", + "redistribution", + "rehearsal-demo" + ] + }, + "assets": [ + { + "path": "late-night-set.wav", + "role": "audio", + "sha256": "cfe5716c4e7489db7f14104513ffc70af199137c2320bd66d7b6bec4f41dacc0", + "bytes": 88244, + "mediaType": "audio/wav" + }, + { + "path": "LICENSE", + "role": "license", + "sha256": "5661642c1414839099e7efc91fdd5bf255df2ceff5bc3549c3b87ae9ada9135d", + "bytes": 524, + "mediaType": "text/plain" + }, + { + "path": "annotations.json", + "role": "annotations", + "sha256": "28236d31e72c58bf11aad3945e3407d8e30e71b223326f9ffb82a83091d73044", + "bytes": 706, + "mediaType": "application/json" + } + ] +} diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index ed4f967bd..fb01c2bac 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -661,6 +661,35 @@ fn select_local_audio_source( Ok(summary) } +#[tauri::command] +fn select_demo_audio_source( + app: tauri::AppHandle, + state: tauri::State<'_, AppState>, +) -> Result { + let resource_dir = app + .path() + .resource_dir() + .map_err(|_| DEMO_UNAVAILABLE_MESSAGE.to_string())?; + let path = resource_dir.join("demo").join(DEMO_AUDIO_FILE_NAME); + let source = validate_demo_audio_source(&path)?; + let project_id = next_project_id(&state); + let project_root = app_owned_root(&app, "projects", &project_id)?; + let cache_root = app_owned_root(&app, "cache", &project_id)?; + let temp_root = app_owned_root(&app, "temp", &project_id)?; + + let summary = ProjectBootstrapSummaryPayload { + project_id, + source_mode: "reference".into(), + project_root: project_root.to_string_lossy().into_owned(), + cache_root: cache_root.to_string_lossy().into_owned(), + temp_root: temp_root.to_string_lossy().into_owned(), + source, + }; + store_bootstrap_source(&state, summary.clone()); + + Ok(summary) +} + #[tauri::command] async fn import_youtube_url( url: String, @@ -870,6 +899,7 @@ fn main() { .manage(AppState::default()) .invoke_handler(tauri::generate_handler![ select_local_audio_source, + select_demo_audio_source, import_youtube_url, start_analysis_job, get_analysis_job_status, diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index 8efaf48c7..b6425e1b4 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -17,11 +17,16 @@ ], "security": { "csp": "default-src 'self'; img-src 'self' asset: data: blob:; style-src 'self'; script-src 'self'; connect-src 'self' ipc: http://ipc.localhost; media-src 'self' asset: data: blob:; font-src 'self' data:", - "capabilities": ["main-capability"] + "capabilities": [ + "main-capability" + ] } }, "bundle": { "active": true, - "targets": "all" + "targets": "all", + "resources": [ + "resources/demo/*" + ] } } diff --git a/apps/desktop/src/App.test.tsx b/apps/desktop/src/App.test.tsx index 3eed386f8..33fb30f6e 100644 --- a/apps/desktop/src/App.test.tsx +++ b/apps/desktop/src/App.test.tsx @@ -19,6 +19,7 @@ const mockLoadProject = vi.fn(); const mockSaveProject = vi.fn(); const mockSubscribeToAnalysisJobUpdates = vi.fn(); let mockLocalAudioSelectionResult: Record | null = null; +let mockDemoAudioSelectionResult: Record | null = null; let mockImportYoutubeUrlError = false; let latestStatusSubscription: ((payload: Record) => void) | null = null; @@ -46,6 +47,7 @@ vi.mock("./lib/analysis", async (importActual) => { roleFocus: ["bass-guitar", "keys-right", "lead-vocal"] }), selectLocalAudioSource: async () => mockLocalAudioSelectionResult ?? actual.selectLocalAudioSource(), + selectDemoAudioSource: async () => mockDemoAudioSelectionResult ?? actual.selectDemoAudioSource(), subscribeToAnalysisJobUpdates: (...args: Parameters) => mockSubscribeToAnalysisJobUpdates(...args), loadProject: () => mockLoadProject(), @@ -191,6 +193,7 @@ describe("App", () => { mockSaveProject.mockReset(); mockSubscribeToAnalysisJobUpdates.mockReset(); mockLocalAudioSelectionResult = null; + mockDemoAudioSelectionResult = null; mockImportYoutubeUrlError = false; latestStatusSubscription = null; mockSubscribeToAnalysisJobUpdates.mockImplementation( @@ -420,6 +423,33 @@ describe("App", () => { }); }); + it("selects the licensed demo through the same local-audio bootstrap", async () => { + tauriInvoke.mockResolvedValueOnce(bootstrapResponse()); + + render(); + + fireEvent.click(screen.getByRole("button", { name: /try the demo/i })); + + await waitFor(() => { + expect(tauriInvoke).toHaveBeenCalledWith("select_demo_audio_source"); + expect(screen.getByText(/start analysis to open tonight's first cue/i)).toBeTruthy(); + }); + }); + + it("names using your own song when the licensed demo cannot load", async () => { + tauriInvoke.mockRejectedValueOnce( + new Error("The licensed demo song could not be loaded. Use your own song to start tonight.") + ); + + render(); + + fireEvent.click(screen.getByRole("button", { name: /try the demo/i })); + + await waitFor(() => { + expect(screen.getByRole("alert")).toHaveTextContent(/use your own song to start tonight/i); + }); + }); + it("shows a safe file-intake error for unsupported local audio selection", async () => { tauriInvoke.mockRejectedValueOnce(new Error("Choose a WAV, MP3, FLAC, or M4A file to start analysis.")); diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index f3d678454..3619210aa 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -40,6 +40,7 @@ import { MAX_YOUTUBE_URL_LENGTH, saveProject, subscribeToAnalysisJobUpdates, + selectDemoAudioSource, selectLocalAudioSource, startAnalysisJob } from "./lib/analysis"; @@ -415,6 +416,10 @@ export function App() { /** Documented. */ const handleChooseLocalAudio = async () => { + if (analysisInFlight || isStarting || isImporting) { + return; + } + setSelectionError(null); setSelectionErrorSource(null); const selection = await selectLocalAudioSource(); @@ -429,6 +434,26 @@ export function App() { setJobStatus(null); }; + /** Validate the bundled licensed demo through the same local-audio bootstrap. */ + const handleTryDemo = async () => { + if (analysisInFlight || isStarting || isImporting) { + return; + } + + setSelectionError(null); + setSelectionErrorSource(null); + const selection = await selectDemoAudioSource(); + if (selection.ok) { + setSelectedBootstrap(selection.bootstrap); + return; + } + + setSelectedBootstrap(null); + setSelectionError(safeErrorDetail(selection.error.message, t("demoUnavailable"))); + setSelectionErrorSource("local"); + setJobStatus(null); + }; + /** Documented. */ const handleImportYoutube = async () => { setSelectionError(null); @@ -514,7 +539,14 @@ export function App() { if (jobResult) { return ; } - return ; + return ( + + ); }; const currentView: RehearsalView = jobResult && activeView === "score" ? "score" : "workspace"; diff --git a/apps/desktop/src/features/workspace/WorkspaceStates.test.tsx b/apps/desktop/src/features/workspace/WorkspaceStates.test.tsx new file mode 100644 index 000000000..7a26d3151 --- /dev/null +++ b/apps/desktop/src/features/workspace/WorkspaceStates.test.tsx @@ -0,0 +1,63 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import { EmptyState, ErrorState, LoadingState } from "./WorkspaceStates"; + +describe("workspace first-run states", () => { + it("names try-the-demo and use-my-own-song as the next actions", () => { + const onTryDemo = vi.fn(); + const onUseOwnSong = vi.fn(); + render(); + + expect(screen.getByRole("heading", { name: /start tonight's rehearsal/i })).toBeTruthy(); + expect(screen.getByText(/your audio stays on this device/i)).toBeTruthy(); + expect(screen.getByText(/original BandScope audio for evaluation/i)).toBeTruthy(); + + fireEvent.click(screen.getByRole("button", { name: /try the demo/i })); + fireEvent.click(screen.getByRole("button", { name: /use my own song/i })); + expect(onTryDemo).toHaveBeenCalledOnce(); + expect(onUseOwnSong).toHaveBeenCalledOnce(); + }); + + it("tells the musician to start analysis after a song is selected", () => { + render( + + ); + + expect(screen.getByText(/start analysis to open tonight's first cue/i)).toBeTruthy(); + expect(screen.getByRole("button", { name: /choose a different song/i })).toBeTruthy(); + }); + + it("does not fire empty-card actions while intake is disabled", () => { + const onTryDemo = vi.fn(); + const onUseOwnSong = vi.fn(); + render(); + + expect(screen.getByRole("button", { name: /try the demo/i })).toBeDisabled(); + expect(screen.getByRole("button", { name: /use my own song/i })).toBeDisabled(); + fireEvent.click(screen.getByRole("button", { name: /try the demo/i })); + fireEvent.click(screen.getByRole("button", { name: /use my own song/i })); + expect(onTryDemo).not.toHaveBeenCalled(); + expect(onUseOwnSong).not.toHaveBeenCalled(); + }); + + it("keeps loading and error copy action-oriented", () => { + render(); + expect(screen.getByRole("status")).toHaveTextContent(/analyzing audio/i); + + render(); + expect(screen.getByRole("alert")).toHaveTextContent(/choose another file/i); + + render(); + expect(screen.getByRole("alert")).toHaveTextContent(/an error occurred during analysis/i); + }); + + it("renders without first-run actions when the parent does not pass them", () => { + render(); + expect(screen.queryByRole("button", { name: /try the demo/i })).toBeNull(); + expect(screen.queryByRole("button", { name: /use my own song/i })).toBeNull(); + }); +}); diff --git a/apps/desktop/src/features/workspace/WorkspaceStates.tsx b/apps/desktop/src/features/workspace/WorkspaceStates.tsx index 8f9aba1b1..61602cce8 100644 --- a/apps/desktop/src/features/workspace/WorkspaceStates.tsx +++ b/apps/desktop/src/features/workspace/WorkspaceStates.tsx @@ -1,10 +1,25 @@ import { createTranslator, detectPreferredLocale } from "../../i18n"; import { Card, CardContent } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; import { Loader2, Music, AlertCircle } from "lucide-react"; -/** Documented. */ -export function EmptyState() { +interface EmptyStateProps { + selectedLabel?: string | null; + disabled?: boolean; + onTryDemo?: () => void; + onUseOwnSong?: () => void; +} + +/** First-run empty workspace: try the licensed demo or use a local song. */ +export function EmptyState({ + selectedLabel = null, + disabled = false, + onTryDemo, + onUseOwnSong +}: EmptyStateProps) { const t = createTranslator(detectPreferredLocale()); + const hasSelection = Boolean(selectedLabel); + return ( @@ -12,7 +27,39 @@ export function EmptyState() { ); diff --git a/apps/desktop/src/lib/analysis.test.ts b/apps/desktop/src/lib/analysis.test.ts index e3347d1f5..89b67f421 100644 --- a/apps/desktop/src/lib/analysis.test.ts +++ b/apps/desktop/src/lib/analysis.test.ts @@ -4,6 +4,7 @@ import { MAX_YOUTUBE_URL_LENGTH, getAnalysisJobStatus, importYoutubeUrl, + selectDemoAudioSource, startAnalysisJob } from "./analysis"; @@ -20,6 +21,30 @@ describe("analysis bridge", () => { delete tauriWindow.__TAURI_INVOKE__; }); + it("fails closed when the licensed demo is requested outside Tauri", async () => { + const selection = await selectDemoAudioSource(); + + expect(selection).toEqual({ + ok: false, + error: { + code: "invalid_request", + message: "The licensed demo song could not be loaded. Use your own song to start tonight." + } + }); + }); + + it("does not invent a browser demo bootstrap when Tauri internals lack invoke", async () => { + tauriWindow.__TAURI_INTERNALS__ = {}; + + const selection = await selectDemoAudioSource(); + + expect(selection.ok).toBe(false); + if (selection.ok) { + throw new Error("browser demo intake must fail closed"); + } + expect(selection.error.message).toMatch(/use your own song/i); + }); + it("imports a standard YouTube URL through the browser fallback when Tauri is absent", async () => { const selection = await importYoutubeUrl("https://www.youtube.com/watch?v=4ozX4yFUC34"); diff --git a/apps/desktop/src/lib/analysis.ts b/apps/desktop/src/lib/analysis.ts index bb750b34b..e4dc62e59 100644 --- a/apps/desktop/src/lib/analysis.ts +++ b/apps/desktop/src/lib/analysis.ts @@ -35,12 +35,15 @@ const BROWSER_PROGRESS_STEPS = [ { progressLabel: "Saving reusable features", progressStage: "persist", progressPercent: 90 } ] as const; const UNSUPPORTED_LOCAL_AUDIO_MESSAGE = "Choose a WAV, MP3, FLAC, or M4A file to start analysis."; +const DEMO_UNAVAILABLE_MESSAGE = + "The licensed demo song could not be loaded. Use your own song to start tonight."; const SAFE_LOCAL_AUDIO_MESSAGES = new Set([ UNSUPPORTED_LOCAL_AUDIO_MESSAGE, "Could not read the selected audio file.", "Could not prepare the local project workspace.", "Could not prepare the local cache workspace.", - "Could not prepare the local temp workspace." + "Could not prepare the local temp workspace.", + DEMO_UNAVAILABLE_MESSAGE ]); const YOUTUBE_VIDEO_ID_PATTERN = /^[A-Za-z0-9_-]{11}$/; const MAX_YOUTUBE_URL_LENGTH = 2000; @@ -132,6 +135,10 @@ async function browserFallback(command: string, args?: Record): throw new Error(UNSUPPORTED_LOCAL_AUDIO_MESSAGE); } + if (command === "select_demo_audio_source") { + throw new Error(DEMO_UNAVAILABLE_MESSAGE); + } + if (command === "get_analysis_job_status") { const jobId = String(args?.jobId ?? ""); const existing = browserJobStore.get(jobId); @@ -244,6 +251,28 @@ export async function selectLocalAudioSource(): Promise { + try { + const response = await invokeAnalysis("select_demo_audio_source"); + return { + ok: true, + bootstrap: parseProjectBootstrapSummary(response) + }; + } catch (error) { + return { + ok: false, + error: { + code: "invalid_request", + message: + error instanceof Error && SAFE_LOCAL_AUDIO_MESSAGES.has(error.message) + ? error.message + : DEMO_UNAVAILABLE_MESSAGE + } + }; + } +} + /** Documented. */ export async function startAnalysisJob(request: AnalysisJobRequest): Promise { let parsedRequest: AnalysisJobRequest; diff --git a/apps/desktop/src/lib/demo.test.ts b/apps/desktop/src/lib/demo.test.ts new file mode 100644 index 000000000..a05fed140 --- /dev/null +++ b/apps/desktop/src/lib/demo.test.ts @@ -0,0 +1,125 @@ +import { createHash } from "node:crypto"; +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { + DEMO_PROVENANCE_KIND, + DEMO_RESOURCE_DIRECTORY, + parseDemoProvenanceManifest, + type DemoProvenanceManifest +} from "./demo"; + +const workspaceRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../../"); + +function bundledManifest(): DemoProvenanceManifest { + const raw = readFileSync( + path.join(workspaceRoot, DEMO_RESOURCE_DIRECTORY, "provenance.json"), + "utf8" + ); + return parseDemoProvenanceManifest(JSON.parse(raw)); +} + +describe("licensed demo provenance", () => { + it("accepts the bundled CC0 package and verifies every recorded hash", () => { + const manifest = bundledManifest(); + expect(manifest.artifactKind).toBe(DEMO_PROVENANCE_KIND); + expect(manifest.song.license).toBe("CC0-1.0"); + expect(manifest.song.title).toBe("Late Night Set"); + expect(manifest.song.permittedUses).toEqual([ + "evaluation", + "redistribution", + "rehearsal-demo" + ]); + for (const asset of manifest.assets) { + const bytes = readFileSync(path.join(workspaceRoot, DEMO_RESOURCE_DIRECTORY, asset.path)); + expect(bytes.byteLength).toBe(asset.bytes); + expect(createHash("sha256").update(bytes).digest("hex")).toBe(asset.sha256); + } + const audio = manifest.assets.find((asset) => asset.role === "audio"); + expect(audio?.path).toBe("late-night-set.wav"); + const wav = readFileSync(path.join(workspaceRoot, DEMO_RESOURCE_DIRECTORY, "late-night-set.wav")); + expect(wav.subarray(0, 4).toString("ascii")).toBe("RIFF"); + expect(wav.subarray(8, 12).toString("ascii")).toBe("WAVE"); + }); + + it("rejects unknown fields, the wrong kind, and a missing asset role", () => { + const manifest = bundledManifest(); + expect(() => parseDemoProvenanceManifest(null)).toThrow(/root/); + expect(() => parseDemoProvenanceManifest([])).toThrow(/root/); + expect(() => parseDemoProvenanceManifest({ ...manifest, extra: true })).toThrow( + /Invalid demo provenance field 'extra'/ + ); + expect(() => parseDemoProvenanceManifest({ ...manifest, artifactKind: "other" })).toThrow( + /artifactKind/ + ); + expect(() => parseDemoProvenanceManifest({ ...manifest, manifestVersion: 2 })).toThrow( + /manifestVersion/ + ); + expect(() => parseDemoProvenanceManifest({ ...manifest, song: null })).toThrow(/song/); + expect(() => + parseDemoProvenanceManifest({ + ...manifest, + song: { ...manifest.song, extra: "nope" } + }) + ).toThrow(/song\.extra/); + expect(() => + parseDemoProvenanceManifest({ + ...manifest, + song: { ...manifest.song, license: "MIT" } + }) + ).toThrow(/license/); + const withoutAudio = { + ...manifest, + assets: manifest.assets.filter((asset) => asset.role !== "audio") + }; + expect(() => parseDemoProvenanceManifest(withoutAudio)).toThrow(/assets/); + }); + + it("rejects traversal paths, non-hex hashes, and malformed assets", () => { + const manifest = bundledManifest(); + const [audio, license, annotations] = manifest.assets; + expect(() => + parseDemoProvenanceManifest({ + ...manifest, + assets: [{ ...audio, path: "../secret.wav" }, license, annotations] + }) + ).toThrow(/assets\[0\]\.path/); + expect(() => + parseDemoProvenanceManifest({ + ...manifest, + assets: [{ ...audio, sha256: "not-a-hash" }, license, annotations] + }) + ).toThrow(/sha256/); + expect(() => + parseDemoProvenanceManifest({ + ...manifest, + assets: [{ ...audio, bytes: 1.5 }, license, annotations] + }) + ).toThrow(/bytes/); + expect(() => + parseDemoProvenanceManifest({ + ...manifest, + assets: [{ ...audio, extra: true }, license, annotations] + }) + ).toThrow(/assets\[0\]\.extra/); + expect(() => + parseDemoProvenanceManifest({ + ...manifest, + assets: [null, license, annotations] + }) + ).toThrow(/assets\[0\]/); + expect(() => + parseDemoProvenanceManifest({ + ...manifest, + assets: [{ ...audio, role: "stems" }, license, annotations] + }) + ).toThrow(/assets\.role/); + expect(() => + parseDemoProvenanceManifest({ + ...manifest, + song: { ...manifest.song, permittedUses: [] } + }) + ).toThrow(/permittedUses/); + }); +}); diff --git a/apps/desktop/src/lib/demo.ts b/apps/desktop/src/lib/demo.ts new file mode 100644 index 000000000..a3a0d0f59 --- /dev/null +++ b/apps/desktop/src/lib/demo.ts @@ -0,0 +1,155 @@ +/** Kind discriminator for the public licensed-demo provenance manifest. */ +export const DEMO_PROVENANCE_KIND = "bandscope.licensed-demo" as const; + +/** Relative directory that Tauri bundles as the licensed demo package. */ +export const DEMO_RESOURCE_DIRECTORY = "apps/desktop/src-tauri/resources/demo"; + +/** Permitted asset roles inside one licensed demo package. */ +export type DemoAssetRole = "audio" | "license" | "annotations"; + +/** One hashed file in the licensed demo package. */ +export type DemoProvenanceAsset = { + path: string; + role: DemoAssetRole; + sha256: string; + bytes: number; + mediaType: string; +}; + +/** Provenance contract for the redistributable BandScope demo song. */ +export type DemoProvenanceManifest = { + manifestVersion: 1; + artifactKind: typeof DEMO_PROVENANCE_KIND; + song: { + id: string; + title: string; + performer: string; + license: "CC0-1.0"; + licenseUrl: string; + permittedUses: string[]; + }; + assets: DemoProvenanceAsset[]; +}; + +const SHA256_PATTERN = /^[a-f0-9]{64}$/; +const RELATIVE_FILE_PATTERN = /^[A-Za-z0-9._-]+$/; +const MAX_MANIFEST_BYTES = 16_384; +const MAX_ASSET_BYTES = 2_000_000; +const REQUIRED_ROLES: DemoAssetRole[] = ["audio", "license", "annotations"]; + +function asNonEmptyString(value: unknown, field: string): string { + if (typeof value !== "string" || value.trim().length === 0 || value.length > 200) { + throw new Error(`Invalid demo provenance field '${field}'`); + } + return value.trim(); +} + +function asAssetRole(value: unknown): DemoAssetRole { + if (value === "audio" || value === "license" || value === "annotations") { + return value; + } + throw new Error("Invalid demo provenance field 'assets.role'"); +} + +/** Parse one licensed-demo provenance manifest and reject unknown fields. */ +export function parseDemoProvenanceManifest(payload: unknown): DemoProvenanceManifest { + if (payload === null || typeof payload !== "object" || Array.isArray(payload)) { + throw new Error("Invalid demo provenance field 'root'"); + } + const record = payload as Record; + const allowed = new Set(["manifestVersion", "artifactKind", "song", "assets"]); + for (const key of Object.keys(record)) { + if (!allowed.has(key)) { + throw new Error(`Invalid demo provenance field '${key}'`); + } + } + if (record.manifestVersion !== 1) { + throw new Error("Invalid demo provenance field 'manifestVersion'"); + } + if (record.artifactKind !== DEMO_PROVENANCE_KIND) { + throw new Error("Invalid demo provenance field 'artifactKind'"); + } + if (record.song === null || typeof record.song !== "object" || Array.isArray(record.song)) { + throw new Error("Invalid demo provenance field 'song'"); + } + const songRecord = record.song as Record; + for (const key of Object.keys(songRecord)) { + if ( + key !== "id" && + key !== "title" && + key !== "performer" && + key !== "license" && + key !== "licenseUrl" && + key !== "permittedUses" + ) { + throw new Error(`Invalid demo provenance field 'song.${key}'`); + } + } + const permittedUses = songRecord.permittedUses; + if (!Array.isArray(permittedUses) || permittedUses.length === 0 || permittedUses.length > 8) { + throw new Error("Invalid demo provenance field 'song.permittedUses'"); + } + const uses = permittedUses.map((entry, index) => asNonEmptyString(entry, `song.permittedUses[${index}]`)); + if (songRecord.license !== "CC0-1.0") { + throw new Error("Invalid demo provenance field 'song.license'"); + } + if (!Array.isArray(record.assets) || record.assets.length !== 3) { + throw new Error("Invalid demo provenance field 'assets'"); + } + const assets: DemoProvenanceAsset[] = record.assets.map((entry, index) => { + if (entry === null || typeof entry !== "object" || Array.isArray(entry)) { + throw new Error(`Invalid demo provenance field 'assets[${index}]'`); + } + const asset = entry as Record; + for (const key of Object.keys(asset)) { + if (key !== "path" && key !== "role" && key !== "sha256" && key !== "bytes" && key !== "mediaType") { + throw new Error(`Invalid demo provenance field 'assets[${index}].${key}'`); + } + } + const assetPath = asNonEmptyString(asset.path, `assets[${index}].path`); + if (!RELATIVE_FILE_PATTERN.test(assetPath)) { + throw new Error(`Invalid demo provenance field 'assets[${index}].path'`); + } + const sha256 = asNonEmptyString(asset.sha256, `assets[${index}].sha256`); + if (!SHA256_PATTERN.test(sha256)) { + throw new Error(`Invalid demo provenance field 'assets[${index}].sha256'`); + } + if ( + !Number.isSafeInteger(asset.bytes) || + (asset.bytes as number) <= 0 || + (asset.bytes as number) > MAX_ASSET_BYTES + ) { + throw new Error(`Invalid demo provenance field 'assets[${index}].bytes'`); + } + return { + path: assetPath, + role: asAssetRole(asset.role), + sha256, + bytes: asset.bytes as number, + mediaType: asNonEmptyString(asset.mediaType, `assets[${index}].mediaType`) + }; + }); + const roles = new Set(assets.map((asset) => asset.role)); + for (const role of REQUIRED_ROLES) { + if (!roles.has(role)) { + throw new Error("Invalid demo provenance field 'assets.role'"); + } + } + const encoded = JSON.stringify(payload); + if (encoded.length > MAX_MANIFEST_BYTES) { + throw new Error("Invalid demo provenance field 'root'"); + } + return { + manifestVersion: 1, + artifactKind: DEMO_PROVENANCE_KIND, + song: { + id: asNonEmptyString(songRecord.id, "song.id"), + title: asNonEmptyString(songRecord.title, "song.title"), + performer: asNonEmptyString(songRecord.performer, "song.performer"), + license: "CC0-1.0", + licenseUrl: asNonEmptyString(songRecord.licenseUrl, "song.licenseUrl"), + permittedUses: uses + }, + assets + }; +} diff --git a/apps/desktop/src/locales/en/common.json b/apps/desktop/src/locales/en/common.json index 39f716d50..e8fe6935a 100644 --- a/apps/desktop/src/locales/en/common.json +++ b/apps/desktop/src/locales/en/common.json @@ -27,9 +27,9 @@ "confidenceLevelHigh": "Ready to trust", "provenanceSourceModel": "Auto-detected", "provenanceSourceUser": "User-confirmed", - "workspaceReadyToAnalyzeTitle": "Ready to Analyze", + "workspaceReadyToAnalyzeTitle": "Start tonight's rehearsal", "workspaceAnalyzingAudioTitle": "Analyzing Audio", - "workspaceEmptyState": "Choose an audio file to prepare for your rehearsal.", + "workspaceEmptyState": "Try the licensed demo or use your own song. Your audio stays on this device.", "workspaceLoadingState": "Analyzing the song's form and instrument roles...", "workspaceErrorState": "An error occurred during analysis. Please try again.", "workspaceRehearsalMapLabel": "Tonight's rehearsal map", @@ -148,5 +148,11 @@ "practiceProgressRegionLabel": "Practice Progress", "practiceProgressLabel": "Practice Progress", "decreasePracticeProgressLabel": "Decrease progress", - "increasePracticeProgressLabel": "Increase progress" + "increasePracticeProgressLabel": "Increase progress", + "tryTheDemo": "Try the demo", + "useMyOwnSong": "Use my own song", + "demoSelectedNextAction": "Start analysis to open tonight's first cue.", + "demoUnavailable": "The licensed demo song could not be loaded. Use your own song to start tonight.", + "demoLimitation": "The demo is original BandScope audio for evaluation, not a commercial track.", + "chooseDifferentSong": "Choose a different song" } diff --git a/apps/desktop/src/locales/ko/common.json b/apps/desktop/src/locales/ko/common.json index 371884abb..0d313110f 100644 --- a/apps/desktop/src/locales/ko/common.json +++ b/apps/desktop/src/locales/ko/common.json @@ -27,9 +27,9 @@ "confidenceLevelHigh": "믿고 가져가도 됨", "provenanceSourceModel": "자동 추정", "provenanceSourceUser": "사용자 확인", - "workspaceReadyToAnalyzeTitle": "분석 준비 완료", + "workspaceReadyToAnalyzeTitle": "오늘 합주를 시작하세요", "workspaceAnalyzingAudioTitle": "오디오 분석 중", - "workspaceEmptyState": "합주할 곡의 오디오 파일을 선택해주세요.", + "workspaceEmptyState": "라이선스가 있는 데모를 써 보거나 내 곡을 사용하세요. 오디오는 이 기기에 남습니다.", "workspaceLoadingState": "곡의 폼과 악기별 역할을 분석하고 있습니다...", "workspaceErrorState": "분석 중 오류가 발생했습니다. 다시 시도해주세요.", "workspaceRehearsalMapLabel": "오늘의 합주 지도", @@ -148,5 +148,11 @@ "practiceProgressRegionLabel": "연습 진척도", "practiceProgressLabel": "연습 진척도", "decreasePracticeProgressLabel": "진척도 감소", - "increasePracticeProgressLabel": "진척도 증가" + "increasePracticeProgressLabel": "진척도 증가", + "tryTheDemo": "데모 써 보기", + "useMyOwnSong": "내 곡 사용", + "demoSelectedNextAction": "분석을 시작해 오늘 첫 큐를 여세요.", + "demoUnavailable": "라이선스 데모 곡을 불러올 수 없습니다. 내 곡으로 오늘 합주를 시작하세요.", + "demoLimitation": "데모는 평가용 오리지널 BandScope 오디오이며 상업 음원이 아닙니다.", + "chooseDifferentSong": "다른 곡 선택" } diff --git a/apps/desktop/vite.config.ts b/apps/desktop/vite.config.ts index f1db6f2b8..8454df2f7 100644 --- a/apps/desktop/vite.config.ts +++ b/apps/desktop/vite.config.ts @@ -22,7 +22,9 @@ export default defineConfig({ include: [ "src/App.tsx", "src/lib/export.ts", + "src/lib/demo.ts", "src/i18n/index.ts", + "src/features/workspace/WorkspaceStates.tsx", "src/features/score/ScoreViewer.tsx", "src/features/score/ScoreView.tsx", "src/features/score/scoreStorage.ts" diff --git a/docs/activation/licensed-demo.md b/docs/activation/licensed-demo.md new file mode 100644 index 000000000..c0e5e7954 --- /dev/null +++ b/docs/activation/licensed-demo.md @@ -0,0 +1,50 @@ +# Licensed demo and first-run rehearsal + +A buyer who launches BandScope with no song loaded must be able to start tonight's rehearsal without locating a file, inventing MIR terminology, or leaving the device. + +This package is the licensed demo slice of #964. It does **not** close the full first-run measurement program, and it does **not** invent a parallel MIR product (#828 still owns #770). + +## What the buyer sees + +1. Open BandScope. +2. The empty workspace names **Try the demo** and **Use my own song**. +3. Privacy copy: your audio stays on this device. +4. **Try the demo** validates the bundled original recording through the same local-audio intake as a user file, then enables **Start analysis**. +5. **Use my own song** opens the existing local-file picker. +6. Analysis is never started automatically. + +Korean and English keep the same choices, limitation, and next action. + +## Licensed package + +Canonical files live in `apps/desktop/src-tauri/resources/demo/`: + +| File | Role | +| --- | --- | +| `late-night-set.wav` | Original two-section evaluation audio (CC0 1.0) | +| `LICENSE` | CC0 1.0 waiver | +| `annotations.json` | Ground-truth verse/chorus times for later #770 evidence | +| `provenance.json` | Exact hashes, byte sizes, performer, and permitted uses | + +`Late Night Set` is original Contextual Wisdom Lab audio. It is not a commercial recording. Private or copyrighted benchmark assets stay out of this public package. + +Regenerate the WAV with `scripts/generate_licensed_demo_wav.py` and refresh `provenance.json` hashes before changing the audio. + +## Production boundary + +`select_demo_audio_source` resolves the bundled WAV from the Tauri resource directory, rejects missing/symlink/non-WAV/wrong-size/non-RIFF files, then reuses the same project/cache/temp bootstrap as `select_local_audio_source`. Browser fallback fails closed and tells the musician to use their own song. No mocked analysis success is presented as a production pass. + +## Security Notes + +- Untrusted input: bundled resource bytes plus the same local-audio bootstrap as a user-selected file. +- Trust boundary: empty-card action → allowlisted Tauri command → resource-dir lookup → size/magic/symlink checks → app-owned project roots. The provenance manifest is not a filesystem authority document and never dereferences user paths or URLs. +- Safe failure: missing or altered demo assets surface payload-free copy that names **Use my own song**. Rejected paths are not rendered. +- Privacy: no telemetry, no demo download, no network path for the bundled audio. +- Test points: provenance hash/size contract, browser fail-closed demo intake, empty-card actions, Rust size/magic/symlink rejection. + +## Out of scope + +- No account, cloud upload, or telemetry consent. +- No copyrighted commercial song. +- No role/goal onboarding form in this slice. +- No dependency, lockfile, or vulnerability-suppression delta. Canonical npm HIGH findings remain #783-owned. diff --git a/scripts/generate_licensed_demo_wav.py b/scripts/generate_licensed_demo_wav.py new file mode 100644 index 000000000..4199a7de7 --- /dev/null +++ b/scripts/generate_licensed_demo_wav.py @@ -0,0 +1,51 @@ +"""Generate the licensed BandScope demo WAV fixture. + +The output is original Contextual Wisdom Lab audio released under CC0 1.0. +""" + +from __future__ import annotations + +import argparse +import math +import struct +import wave +from pathlib import Path + +SAMPLE_RATE = 22050 +DURATION_SECONDS = 2 +AMPLITUDE = 0.2 + + +def write_demo_wav(path: Path) -> None: + """Write the two-section sine WAV used by the licensed demo package.""" + path.parent.mkdir(parents=True, exist_ok=True) + n_frames = SAMPLE_RATE * DURATION_SECONDS + with wave.open(str(path), "wb") as wav_file: + wav_file.setnchannels(1) + wav_file.setsampwidth(2) + wav_file.setframerate(SAMPLE_RATE) + frames = bytearray() + for index in range(n_frames): + moment = index / SAMPLE_RATE + frequency = 220.0 if moment < 1.0 else 330.0 + sample = int(AMPLITUDE * 32767.0 * math.sin(2.0 * math.pi * frequency * moment)) + frames.extend(struct.pack(" int: + """Write ``late-night-set.wav`` to the bundled demo resource directory.""" + parser = argparse.ArgumentParser(description="Generate the licensed BandScope demo WAV.") + parser.add_argument( + "--output", + type=Path, + default=Path("apps/desktop/src-tauri/resources/demo/late-night-set.wav"), + help="Destination WAV path.", + ) + args = parser.parse_args() + write_demo_wav(args.output) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From eded10b25466557687eb843572b0f72256d2a0ed Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 14:13:41 -0700 Subject: [PATCH 02/20] test(activation): bind demo bundle destination to runtime lookup --- .../src-tauri/tests/demo_resource_contract.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 apps/desktop/src-tauri/tests/demo_resource_contract.rs diff --git a/apps/desktop/src-tauri/tests/demo_resource_contract.rs b/apps/desktop/src-tauri/tests/demo_resource_contract.rs new file mode 100644 index 000000000..e3d98d628 --- /dev/null +++ b/apps/desktop/src-tauri/tests/demo_resource_contract.rs @@ -0,0 +1,20 @@ +use std::path::PathBuf; + +use serde_json::Value; + +#[test] +fn bundled_demo_is_mapped_to_runtime_demo_directory() { + let config_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tauri.conf.json"); + let config: Value = serde_json::from_str( + &std::fs::read_to_string(config_path).expect("Tauri config should be readable"), + ) + .expect("Tauri config should contain valid JSON"); + + let destination = config + .pointer("/bundle/resources") + .and_then(Value::as_object) + .and_then(|resources| resources.get("resources/demo/*")) + .and_then(Value::as_str); + + assert_eq!(destination, Some("demo/")); +} From 2d1de846dc27b984fdf231659e13b8b9bbe55900 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 14:13:54 -0700 Subject: [PATCH 03/20] fix(activation): align bundled demo destination with runtime lookup --- apps/desktop/src-tauri/tauri.conf.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index b6425e1b4..c7d15921b 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -25,8 +25,8 @@ "bundle": { "active": true, "targets": "all", - "resources": [ - "resources/demo/*" - ] + "resources": { + "resources/demo/*": "demo/" + } } } From 9ced398d7f56320b578f2367eac0e7c50eee5497 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 14:15:21 -0700 Subject: [PATCH 04/20] fix(activation): satisfy licensed-demo public documentation gate --- apps/desktop/src/lib/demo.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/lib/demo.ts b/apps/desktop/src/lib/demo.ts index a3a0d0f59..ffd4dba48 100644 --- a/apps/desktop/src/lib/demo.ts +++ b/apps/desktop/src/lib/demo.ts @@ -1,8 +1,10 @@ /** Kind discriminator for the public licensed-demo provenance manifest. */ -export const DEMO_PROVENANCE_KIND = "bandscope.licensed-demo" as const; +const DEMO_PROVENANCE_KIND = "bandscope.licensed-demo" as const; /** Relative directory that Tauri bundles as the licensed demo package. */ -export const DEMO_RESOURCE_DIRECTORY = "apps/desktop/src-tauri/resources/demo"; +const DEMO_RESOURCE_DIRECTORY = "apps/desktop/src-tauri/resources/demo"; + +export { DEMO_PROVENANCE_KIND, DEMO_RESOURCE_DIRECTORY }; /** Permitted asset roles inside one licensed demo package. */ export type DemoAssetRole = "audio" | "license" | "annotations"; @@ -37,6 +39,7 @@ const MAX_MANIFEST_BYTES = 16_384; const MAX_ASSET_BYTES = 2_000_000; const REQUIRED_ROLES: DemoAssetRole[] = ["audio", "license", "annotations"]; +/** Read one bounded non-empty provenance string. */ function asNonEmptyString(value: unknown, field: string): string { if (typeof value !== "string" || value.trim().length === 0 || value.length > 200) { throw new Error(`Invalid demo provenance field '${field}'`); @@ -44,6 +47,7 @@ function asNonEmptyString(value: unknown, field: string): string { return value.trim(); } +/** Narrow one untrusted asset role to the licensed-demo allowlist. */ function asAssetRole(value: unknown): DemoAssetRole { if (value === "audio" || value === "license" || value === "annotations") { return value; From 22ba7ae5dc21566cfce39f5c1e85e80eb57af97d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 14:17:39 -0700 Subject: [PATCH 05/20] test(analysis): require browser analysis to fail closed --- apps/desktop/src/lib/analysis.test.ts | 51 +++++---------------------- 1 file changed, 9 insertions(+), 42 deletions(-) diff --git a/apps/desktop/src/lib/analysis.test.ts b/apps/desktop/src/lib/analysis.test.ts index 89b67f421..d18097406 100644 --- a/apps/desktop/src/lib/analysis.test.ts +++ b/apps/desktop/src/lib/analysis.test.ts @@ -142,50 +142,17 @@ describe("analysis bridge", () => { expect(status.result?.sections[0]?.timeRange).toEqual({ start: 0, end: 1 }); }); - it("reports staged browser fallback progress before returning the demo result", async () => { - const queued = await startAnalysisJob(createDemoAnalysisJobRequest()); - - expect(queued).toMatchObject({ - state: "queued", - progressLabel: "Queued for analysis", - progressStage: "queued", - progressPercent: 0 - }); - - const running = await getAnalysisJobStatus(queued.jobId); - expect(running).toMatchObject({ - state: "running", - progressLabel: "Decoding audio", - progressStage: "decode", - progressPercent: 20 - }); - - expect(await getAnalysisJobStatus(queued.jobId)).toMatchObject({ - state: "running", - progressLabel: "Separating stems... (45%)", - progressStage: "separate", - progressPercent: 45 - }); - expect(await getAnalysisJobStatus(queued.jobId)).toMatchObject({ - state: "running", - progressLabel: "Building rehearsal cues", - progressStage: "analyze", - progressPercent: 70 - }); - expect(await getAnalysisJobStatus(queued.jobId)).toMatchObject({ - state: "running", - progressLabel: "Saving reusable features", - progressStage: "persist", - progressPercent: 90 - }); + it("fails browser analysis closed instead of synthesizing a rehearsal result", async () => { + const status = await startAnalysisJob(createDemoAnalysisJobRequest()); - const ready = await getAnalysisJobStatus(queued.jobId); - expect(ready).toMatchObject({ - state: "succeeded", - progressLabel: "Analysis ready", - progressStage: "ready", - progressPercent: 100 + expect(status).toMatchObject({ + state: "failed", + error: { + code: "engine_unavailable", + message: "Analysis engine is unavailable." + } }); + expect(status.result).toBeNull(); }); it("ignores a non-function Tauri v1 invoke shim", async () => { From cfb2cff5a82eb3bbaa04c669b4c4ee0351c2682d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 14:18:24 -0700 Subject: [PATCH 06/20] fix(analysis): fail browser analysis closed --- apps/desktop/src/lib/analysis.ts | 76 ++++++++------------------------ 1 file changed, 19 insertions(+), 57 deletions(-) diff --git a/apps/desktop/src/lib/analysis.ts b/apps/desktop/src/lib/analysis.ts index e4dc62e59..c87010fd7 100644 --- a/apps/desktop/src/lib/analysis.ts +++ b/apps/desktop/src/lib/analysis.ts @@ -2,7 +2,6 @@ import { invoke } from "@tauri-apps/api/core"; import { createAnalysisJobStatus, createDemoAnalysisJobRequest, - createDemoRehearsalSong, createProjectBootstrapSummary, parseAnalysisJobStatus, parseAnalysisJobRequest, @@ -27,13 +26,6 @@ declare global { } } -const browserJobStore = new Map(); -const BROWSER_PROGRESS_STEPS = [ - { progressLabel: "Decoding audio", progressStage: "decode", progressPercent: 20 }, - { progressLabel: "Separating stems... (45%)", progressStage: "separate", progressPercent: 45 }, - { progressLabel: "Building rehearsal cues", progressStage: "analyze", progressPercent: 70 }, - { progressLabel: "Saving reusable features", progressStage: "persist", progressPercent: 90 } -] as const; const UNSUPPORTED_LOCAL_AUDIO_MESSAGE = "Choose a WAV, MP3, FLAC, or M4A file to start analysis."; const DEMO_UNAVAILABLE_MESSAGE = "The licensed demo song could not be loaded. Use your own song to start tonight."; @@ -118,17 +110,14 @@ function browserJobId(prefix: string): string { async function browserFallback(command: string, args?: Record): Promise { if (command === "start_analysis_job") { parseAnalysisJobRequest(args?.request); - const jobId = browserJobId("browser-job"); - const queued = createAnalysisJobStatus({ - jobId, - state: "queued", - progressLabel: "Queued for analysis", - progressStage: "queued", - progressPercent: 0, - cacheStatus: "disabled" + return createAnalysisJobStatus({ + jobId: browserJobId("browser-job"), + state: "failed", + error: { + code: "engine_unavailable", + message: "Analysis engine is unavailable." + } }); - browserJobStore.set(jobId, queued); - return queued; } if (command === "select_local_audio_source") { @@ -141,46 +130,14 @@ async function browserFallback(command: string, args?: Record): if (command === "get_analysis_job_status") { const jobId = String(args?.jobId ?? ""); - const existing = browserJobStore.get(jobId); - if (!existing) { - return createAnalysisJobStatus({ - jobId, - state: "failed", - error: { - code: "not_found", - message: "Analysis job was not found." - } - }); - } - if (existing.state === "queued" || existing.state === "running") { - const currentPercent = existing.progressPercent ?? 0; - const nextStep = BROWSER_PROGRESS_STEPS.find((step) => step.progressPercent > currentPercent); - if (nextStep) { - const running = createAnalysisJobStatus({ - jobId, - state: "running", - requestedAt: existing.requestedAt, - progressLabel: nextStep.progressLabel, - progressStage: nextStep.progressStage, - progressPercent: nextStep.progressPercent, - cacheStatus: "disabled" - }); - browserJobStore.set(jobId, running); - return running; - } - } - const succeeded = createAnalysisJobStatus({ + return createAnalysisJobStatus({ jobId, - state: "succeeded", - progressLabel: "Analysis ready", - progressStage: "ready", - progressPercent: 100, - cacheStatus: "disabled", - requestedAt: existing.requestedAt, - result: createDemoRehearsalSong() + state: "failed", + error: { + code: "not_found", + message: "Analysis job was not found." + } }); - browserJobStore.set(jobId, succeeded); - return succeeded; } if (command === "save_project") { @@ -360,7 +317,12 @@ export async function importYoutubeUrl(url: string): Promise Date: Sun, 23 Aug 2026 14:18:56 -0700 Subject: [PATCH 07/20] docs(analysis): state browser analysis fail-closed contract --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 70c8dd773..96c3c35f7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,7 +51,7 @@ BandScope is a local-first desktop app for rehearsal prep: it turns a song into Three layers, decoupled through shared contracts: -- `apps/desktop` — Tauri 2 + Vite + React 19 shell (Tailwind 4, Base UI, Storybook). Feature screens live in `src/features/` (home, workspace, chords, ranges, player, settings). `src/lib/analysis.ts` and `src/lib/job_runner.ts` call typed Tauri IPC commands, with a browser fallback that serves demo analysis data when not running inside Tauri. Bundled licensed-demo intake is desktop-only and fails closed in the browser. +- `apps/desktop` — Tauri 2 + Vite + React 19 shell (Tailwind 4, Base UI, Storybook). Feature screens live in `src/features/` (home, workspace, chords, ranges, player, settings). `src/lib/analysis.ts` and `src/lib/job_runner.ts` call typed Tauri IPC commands. Without the desktop IPC bridge, analysis fails closed instead of synthesizing a production success; bundled licensed-demo intake is desktop-only and also fails closed in the browser. - `apps/desktop/src-tauri/src/main.rs` — the Rust orchestration boundary. Tauri commands (`start_analysis_job`, `get_analysis_job_status`, `select_local_audio_source`, `select_demo_audio_source`, `import_youtube_url`) validate untrusted input (project IDs, file paths, URLs) and spawn the Python engine as a subprocess. There is no loopback HTTP listener and no network path for local analysis. - `services/analysis-engine` — Python package `bandscope_analysis` (librosa/numpy). Entry point `cli.py` reads a JSON job request on stdin and prints a structured job-status JSON envelope on stdout (`--progress-jsonl` streams progress lines). `api.py` orchestrates the pipeline across the `separation`, `sections`, `roles`, `chords`, `ranges`, `temporal`, `transcription`, and `youtube` modules. From 1ece98e5dec73925db4e3c5925f8fd670597017f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 17:15:01 -0700 Subject: [PATCH 08/20] fix(licensing): remove demo CC0 usage ambiguity --- apps/desktop/src-tauri/resources/demo/LICENSE | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src-tauri/resources/demo/LICENSE b/apps/desktop/src-tauri/resources/demo/LICENSE index b91a8da51..6fc45a45a 100644 --- a/apps/desktop/src-tauri/resources/demo/LICENSE +++ b/apps/desktop/src-tauri/resources/demo/LICENSE @@ -1,12 +1,17 @@ CC0 1.0 Universal -Late Night Set is original audio created for BandScope evaluation by -Contextual Wisdom Lab. To the extent possible under law, the author has -waived all copyright and related or neighboring rights to this demo -package (audio, annotations, and this license text). +Late Night Set is original audio created for BandScope by Contextual Wisdom Lab. +To the extent possible under law, the rights holder has waived all copyright +and related or neighboring rights in the bundled demo package (audio, +annotations, and accompanying provenance metadata) under CC0 1.0 Universal. -You may copy, modify, redistribute, and perform the demo for product -evaluation and rehearsal. This is not a commercial recording and must -not be presented as one. +No additional use restriction is imposed by this file. CC0 permits reuse for +any purpose, including commercial, advertising, and promotional purposes, +subject to the limitations and disclaimers in the canonical legal code. -Full legal text: https://creativecommons.org/publicdomain/zero/1.0/legalcode +Canonical CC0 1.0 legal code: +https://creativecommons.org/publicdomain/zero/1.0/legalcode + +Product notice (not a license condition): the bundled recording was created as +a BandScope evaluation and rehearsal demo. It is not represented as a +third-party commercial recording. From 5bddc15bde2612400a108b2148a7abdcdd67bb20 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 17:15:21 -0700 Subject: [PATCH 09/20] docs(activation): correct demo provenance refresh order --- docs/activation/licensed-demo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/activation/licensed-demo.md b/docs/activation/licensed-demo.md index c0e5e7954..72796421e 100644 --- a/docs/activation/licensed-demo.md +++ b/docs/activation/licensed-demo.md @@ -28,7 +28,7 @@ Canonical files live in `apps/desktop/src-tauri/resources/demo/`: `Late Night Set` is original Contextual Wisdom Lab audio. It is not a commercial recording. Private or copyrighted benchmark assets stay out of this public package. -Regenerate the WAV with `scripts/generate_licensed_demo_wav.py` and refresh `provenance.json` hashes before changing the audio. +After changing the source audio, regenerate the WAV with `scripts/generate_licensed_demo_wav.py`, then refresh `provenance.json` with the final file's byte size and SHA-256 before packaging or committing it. ## Production boundary From e11c5283bdbb4281f0931c53506b354e4f18a654 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 18:03:09 -0700 Subject: [PATCH 10/20] test(security): reject demo provenance dot segments --- apps/desktop/src/lib/demo.test.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/demo.test.ts b/apps/desktop/src/lib/demo.test.ts index a05fed140..939e4fdab 100644 --- a/apps/desktop/src/lib/demo.test.ts +++ b/apps/desktop/src/lib/demo.test.ts @@ -76,7 +76,7 @@ describe("licensed demo provenance", () => { expect(() => parseDemoProvenanceManifest(withoutAudio)).toThrow(/assets/); }); - it("rejects traversal paths, non-hex hashes, and malformed assets", () => { + it("rejects traversal paths, dot segments, non-hex hashes, and malformed assets", () => { const manifest = bundledManifest(); const [audio, license, annotations] = manifest.assets; expect(() => @@ -85,6 +85,14 @@ describe("licensed demo provenance", () => { assets: [{ ...audio, path: "../secret.wav" }, license, annotations] }) ).toThrow(/assets\[0\]\.path/); + for (const dotSegment of [".", ".."]) { + expect(() => + parseDemoProvenanceManifest({ + ...manifest, + assets: [{ ...audio, path: dotSegment }, license, annotations] + }) + ).toThrow(/assets\[0\]\.path/); + } expect(() => parseDemoProvenanceManifest({ ...manifest, From cffdd22b6859f7ab3575fd40209d027d0d63edab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 18:03:38 -0700 Subject: [PATCH 11/20] fix(security): reject demo provenance dot segments --- apps/desktop/src/lib/demo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/demo.ts b/apps/desktop/src/lib/demo.ts index ffd4dba48..5f3ee2417 100644 --- a/apps/desktop/src/lib/demo.ts +++ b/apps/desktop/src/lib/demo.ts @@ -111,7 +111,7 @@ export function parseDemoProvenanceManifest(payload: unknown): DemoProvenanceMan } } const assetPath = asNonEmptyString(asset.path, `assets[${index}].path`); - if (!RELATIVE_FILE_PATTERN.test(assetPath)) { + if (assetPath === "." || assetPath === ".." || !RELATIVE_FILE_PATTERN.test(assetPath)) { throw new Error(`Invalid demo provenance field 'assets[${index}].path'`); } const sha256 = asNonEmptyString(asset.sha256, `assets[${index}].sha256`); From 90c13ae68e728e538d84e79473b50ac3f1bbb600 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:03:28 -0700 Subject: [PATCH 12/20] fix(demo): refresh license provenance metadata --- apps/desktop/src-tauri/resources/demo/provenance.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src-tauri/resources/demo/provenance.json b/apps/desktop/src-tauri/resources/demo/provenance.json index 307e49d06..26e0f952b 100644 --- a/apps/desktop/src-tauri/resources/demo/provenance.json +++ b/apps/desktop/src-tauri/resources/demo/provenance.json @@ -24,8 +24,8 @@ { "path": "LICENSE", "role": "license", - "sha256": "5661642c1414839099e7efc91fdd5bf255df2ceff5bc3549c3b87ae9ada9135d", - "bytes": 524, + "sha256": "1657b89949ca8bfb2920e26dceb4c1012d6212b5d77eda7d7f3921da29adde5e", + "bytes": 822, "mediaType": "text/plain" }, { @@ -36,4 +36,4 @@ "mediaType": "application/json" } ] -} +} \ No newline at end of file From 7ffd39b3199d678e797fab04b1e37867ca8be29c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:04:29 -0700 Subject: [PATCH 13/20] test(analysis): align failed status contract --- apps/desktop/src/lib/analysis.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/analysis.test.ts b/apps/desktop/src/lib/analysis.test.ts index d18097406..b9b7a8c74 100644 --- a/apps/desktop/src/lib/analysis.test.ts +++ b/apps/desktop/src/lib/analysis.test.ts @@ -152,7 +152,7 @@ describe("analysis bridge", () => { message: "Analysis engine is unavailable." } }); - expect(status.result).toBeNull(); + expect(status.result).toBeUndefined(); }); it("ignores a non-function Tauri v1 invoke shim", async () => { From c8015b804e1559a40331d205f57988eb9ce3672e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 23 Aug 2026 19:05:48 -0700 Subject: [PATCH 14/20] test(workspace): isolate state transitions --- .../desktop/src/features/workspace/WorkspaceStates.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/features/workspace/WorkspaceStates.test.tsx b/apps/desktop/src/features/workspace/WorkspaceStates.test.tsx index 7a26d3151..6eff92b93 100644 --- a/apps/desktop/src/features/workspace/WorkspaceStates.test.tsx +++ b/apps/desktop/src/features/workspace/WorkspaceStates.test.tsx @@ -45,13 +45,13 @@ describe("workspace first-run states", () => { }); it("keeps loading and error copy action-oriented", () => { - render(); + const { rerender } = render(); expect(screen.getByRole("status")).toHaveTextContent(/analyzing audio/i); - render(); + rerender(); expect(screen.getByRole("alert")).toHaveTextContent(/choose another file/i); - render(); + rerender(); expect(screen.getByRole("alert")).toHaveTextContent(/an error occurred during analysis/i); }); From 0200dca1feae7bf0cf931435fa254c887e6a08ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 11:16:11 +0900 Subject: [PATCH 15/20] test(workspace): align licensed demo state assertions --- apps/desktop/src/App.test.tsx | 2 +- apps/desktop/src/features/workspace/Workspace.test.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/App.test.tsx b/apps/desktop/src/App.test.tsx index 33fb30f6e..b3238ecbc 100644 --- a/apps/desktop/src/App.test.tsx +++ b/apps/desktop/src/App.test.tsx @@ -431,7 +431,7 @@ describe("App", () => { fireEvent.click(screen.getByRole("button", { name: /try the demo/i })); await waitFor(() => { - expect(tauriInvoke).toHaveBeenCalledWith("select_demo_audio_source"); + expect(tauriInvoke).toHaveBeenCalledWith("select_demo_audio_source", undefined); expect(screen.getByText(/start analysis to open tonight's first cue/i)).toBeTruthy(); }); }); diff --git a/apps/desktop/src/features/workspace/Workspace.test.tsx b/apps/desktop/src/features/workspace/Workspace.test.tsx index a3da5ffe6..d224089b7 100644 --- a/apps/desktop/src/features/workspace/Workspace.test.tsx +++ b/apps/desktop/src/features/workspace/Workspace.test.tsx @@ -248,7 +248,7 @@ describe("Workspace", () => { render(); render(); - expect(screen.getByRole("heading", { name: "분석 준비 완료" })).toBeTruthy(); + expect(screen.getByRole("heading", { name: "오늘 합주를 시작하세요" })).toBeTruthy(); expect(screen.getByRole("heading", { name: "오디오 분석 중" })).toBeTruthy(); }); From b2082e583f683986ac19f4efb3b255c312beaac3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 11:36:57 +0900 Subject: [PATCH 16/20] fix: close licensed demo review findings --- apps/desktop/core/src/lib.rs | 106 +++++++++++++++--- apps/desktop/src/App.test.tsx | 22 ++++ apps/desktop/src/App.tsx | 39 +++++-- .../workspace/WorkspaceStates.test.tsx | 14 +++ .../features/workspace/WorkspaceStates.tsx | 8 +- apps/desktop/src/locales/en/common.json | 1 + apps/desktop/src/locales/ko/common.json | 1 + 7 files changed, 165 insertions(+), 26 deletions(-) diff --git a/apps/desktop/core/src/lib.rs b/apps/desktop/core/src/lib.rs index 1f8273215..f914b99bd 100644 --- a/apps/desktop/core/src/lib.rs +++ b/apps/desktop/core/src/lib.rs @@ -666,11 +666,6 @@ pub fn validate_score_pdf_source(path: &Path) -> Result<(PathBuf, String, u64), Ok((canonical, file_name, file_size_bytes)) } -/// Security Notes: reads and deletes never accept an arbitrary path from the -/// WebView. The path is rebuilt server-side from validated ids, symlinks are -/// refused, and the canonicalized result must still live under the -/// canonicalized app-owned scores root (path-traversal guard). - /// Validate the bundled licensed demo WAV before it reuses local-audio bootstrap. pub fn validate_demo_audio_source(path: &Path) -> Result { let link_metadata = std::fs::symlink_metadata(path) @@ -708,11 +703,8 @@ pub fn validate_demo_audio_source(path: &Path) -> Result Result bool { + if data.len() < 44 + || &data[0..4] != WAV_RIFF_MAGIC + || &data[8..12] != WAV_WAVE_MAGIC + || u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize != data.len() - 8 + { + return false; + } + + let mut offset = 12usize; + let mut has_format = false; + let mut has_data = false; + while offset < data.len() { + let header_end = match offset.checked_add(8) { + Some(value) if value <= data.len() => value, + _ => return false, + }; + let chunk_size = u32::from_le_bytes([ + data[offset + 4], + data[offset + 5], + data[offset + 6], + data[offset + 7], + ]) as usize; + let chunk_end = match header_end.checked_add(chunk_size) { + Some(value) if value <= data.len() => value, + _ => return false, + }; + let next_offset = match chunk_end.checked_add(chunk_size % 2) { + Some(value) if value <= data.len() => value, + _ => return false, + }; + + match &data[offset..offset + 4] { + b"fmt " if chunk_size >= 16 => { + let format = u16::from_le_bytes([data[header_end], data[header_end + 1]]); + let channels = u16::from_le_bytes([data[header_end + 2], data[header_end + 3]]); + let sample_rate = u32::from_le_bytes([ + data[header_end + 4], + data[header_end + 5], + data[header_end + 6], + data[header_end + 7], + ]); + let byte_rate = u32::from_le_bytes([ + data[header_end + 8], + data[header_end + 9], + data[header_end + 10], + data[header_end + 11], + ]); + let block_align = u16::from_le_bytes([data[header_end + 12], data[header_end + 13]]); + let bits_per_sample = + u16::from_le_bytes([data[header_end + 14], data[header_end + 15]]); + has_format = format == 1 + && channels > 0 + && sample_rate > 0 + && byte_rate > 0 + && block_align > 0 + && bits_per_sample > 0; + } + b"data" if chunk_size > 0 => has_data = true, + _ => {} + } + + offset = next_offset; + } + + offset == data.len() && has_format && has_data +} + +/// Security Notes: reads and deletes never accept an arbitrary path from the +/// WebView. The path is rebuilt server-side from validated ids, symlinks are +/// refused, and the canonicalized result must still live under the +/// canonicalized app-owned scores root (path-traversal guard). pub fn resolve_existing_score_pdf(scores_root: &Path, score_id: &str) -> Result { if !is_valid_score_id(score_id) { return Err("Score was not found.".to_string()); @@ -1348,11 +1412,21 @@ mod tests { fn write_sized_demo_wav(path: &Path, bytes: u64, riff: bool, wave: bool) { let mut data = vec![0u8; bytes as usize]; - if riff && data.len() >= 4 { + if riff && wave && data.len() >= 44 { + let data_len = data.len() as u32; data[0..4].copy_from_slice(b"RIFF"); - } - if wave && data.len() >= 12 { + data[4..8].copy_from_slice(&(data_len - 8).to_le_bytes()); data[8..12].copy_from_slice(b"WAVE"); + data[12..16].copy_from_slice(b"fmt "); + data[16..20].copy_from_slice(&16u32.to_le_bytes()); + data[20..22].copy_from_slice(&1u16.to_le_bytes()); + data[22..24].copy_from_slice(&1u16.to_le_bytes()); + data[24..28].copy_from_slice(&44_100u32.to_le_bytes()); + data[28..32].copy_from_slice(&88_200u32.to_le_bytes()); + data[32..34].copy_from_slice(&2u16.to_le_bytes()); + data[34..36].copy_from_slice(&16u16.to_le_bytes()); + data[36..40].copy_from_slice(b"data"); + data[40..44].copy_from_slice(&(data_len - 44).to_le_bytes()); } std::fs::write(path, data).expect("demo fixture should be written"); } @@ -1383,6 +1457,12 @@ mod tests { write_sized_demo_wav(&wrong_size, 12, true, true); assert!(validate_demo_audio_source(&wrong_size).is_err()); + write_sized_demo_wav(&wrong_size, DEMO_AUDIO_BYTES, true, true); + let mut malformed = std::fs::read(&wrong_size).expect("valid demo fixture should be readable"); + malformed[4..8].copy_from_slice(&(DEMO_AUDIO_BYTES as u32).to_le_bytes()); + std::fs::write(&wrong_size, malformed).expect("malformed fixture should be writable"); + assert!(validate_demo_audio_source(&wrong_size).is_err()); + write_sized_demo_wav(&wrong_size, DEMO_AUDIO_BYTES, false, true); assert!(validate_demo_audio_source(&wrong_size).is_err()); diff --git a/apps/desktop/src/App.test.tsx b/apps/desktop/src/App.test.tsx index b3238ecbc..c6a6ed977 100644 --- a/apps/desktop/src/App.test.tsx +++ b/apps/desktop/src/App.test.tsx @@ -436,6 +436,28 @@ describe("App", () => { }); }); + it("does not issue duplicate demo selection requests while intake is pending", async () => { + let resolveSelection: ((value: unknown) => void) | null = null; + tauriInvoke.mockImplementation( + () => new Promise((resolve) => { + resolveSelection = resolve; + }) + ); + + render(); + + const tryDemo = screen.getByRole("button", { name: /try the demo/i }); + fireEvent.click(tryDemo); + fireEvent.click(tryDemo); + + expect(tauriInvoke).toHaveBeenCalledTimes(1); + await act(async () => { + resolveSelection?.(bootstrapResponse()); + await Promise.resolve(); + }); + expect(await screen.findByText(/start analysis to open tonight's first cue/i)).toBeTruthy(); + }); + it("names using your own song when the licensed demo cannot load", async () => { tauriInvoke.mockRejectedValueOnce( new Error("The licensed demo song could not be loaded. Use your own song to start tonight.") diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 3619210aa..1990c5e42 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -259,6 +259,8 @@ export function App() { const [renderedProgressPercent, setRenderedProgressPercent] = useState(undefined); const [isStarting, setIsStarting] = useState(false); const [selectedBootstrap, setSelectedBootstrap] = useState(null); + const [selectedSourceKind, setSelectedSourceKind] = useState<"demo" | "local" | null>(null); + const [isSelectingDemo, setIsSelectingDemo] = useState(false); const [activeAnalysisBootstrap, setActiveAnalysisBootstrap] = useState(null); const [selectionError, setSelectionError] = useState(null); const [selectionErrorSource, setSelectionErrorSource] = useState<"local" | "youtube" | null>(null); @@ -266,6 +268,7 @@ export function App() { const [isImporting, setIsImporting] = useState(false); const [activeView, setActiveView] = useState("workspace"); const activeJobIdRef = useRef(null); + const demoSelectionInFlightRef = useRef(false); const youtubeInputRef = useRef(null); const analysisInFlight = jobStatus?.state === "queued" || jobStatus?.state === "running"; @@ -416,7 +419,7 @@ export function App() { /** Documented. */ const handleChooseLocalAudio = async () => { - if (analysisInFlight || isStarting || isImporting) { + if (analysisInFlight || isStarting || isImporting || isSelectingDemo) { return; } @@ -425,10 +428,12 @@ export function App() { const selection = await selectLocalAudioSource(); if (selection.ok) { setSelectedBootstrap(selection.bootstrap); + setSelectedSourceKind("local"); return; } setSelectedBootstrap(null); + setSelectedSourceKind(null); setSelectionError(safeErrorDetail(selection.error.message, t("unsupportedLocalAudio"))); setSelectionErrorSource("local"); setJobStatus(null); @@ -436,22 +441,31 @@ export function App() { /** Validate the bundled licensed demo through the same local-audio bootstrap. */ const handleTryDemo = async () => { - if (analysisInFlight || isStarting || isImporting) { + if (analysisInFlight || isStarting || isImporting || demoSelectionInFlightRef.current) { return; } + demoSelectionInFlightRef.current = true; + setIsSelectingDemo(true); setSelectionError(null); setSelectionErrorSource(null); - const selection = await selectDemoAudioSource(); - if (selection.ok) { - setSelectedBootstrap(selection.bootstrap); - return; - } + try { + const selection = await selectDemoAudioSource(); + if (selection.ok) { + setSelectedBootstrap(selection.bootstrap); + setSelectedSourceKind("demo"); + return; + } - setSelectedBootstrap(null); - setSelectionError(safeErrorDetail(selection.error.message, t("demoUnavailable"))); - setSelectionErrorSource("local"); - setJobStatus(null); + setSelectedBootstrap(null); + setSelectedSourceKind(null); + setSelectionError(safeErrorDetail(selection.error.message, t("demoUnavailable"))); + setSelectionErrorSource("local"); + setJobStatus(null); + } finally { + demoSelectionInFlightRef.current = false; + setIsSelectingDemo(false); + } }; /** Documented. */ @@ -542,7 +556,8 @@ export function App() { return ( diff --git a/apps/desktop/src/features/workspace/WorkspaceStates.test.tsx b/apps/desktop/src/features/workspace/WorkspaceStates.test.tsx index 6eff92b93..5f4227987 100644 --- a/apps/desktop/src/features/workspace/WorkspaceStates.test.tsx +++ b/apps/desktop/src/features/workspace/WorkspaceStates.test.tsx @@ -31,6 +31,20 @@ describe("workspace first-run states", () => { expect(screen.getByRole("button", { name: /choose a different song/i })).toBeTruthy(); }); + it("uses general next-step copy for a locally selected song", () => { + render( + + ); + + expect(screen.getByText(/start analysis to open your first cue/i)).toBeTruthy(); + expect(screen.queryByText(/tonight's first cue/i)).toBeNull(); + }); + it("does not fire empty-card actions while intake is disabled", () => { const onTryDemo = vi.fn(); const onUseOwnSong = vi.fn(); diff --git a/apps/desktop/src/features/workspace/WorkspaceStates.tsx b/apps/desktop/src/features/workspace/WorkspaceStates.tsx index 61602cce8..f24507d06 100644 --- a/apps/desktop/src/features/workspace/WorkspaceStates.tsx +++ b/apps/desktop/src/features/workspace/WorkspaceStates.tsx @@ -5,6 +5,7 @@ import { Loader2, Music, AlertCircle } from "lucide-react"; interface EmptyStateProps { selectedLabel?: string | null; + selectedKind?: "demo" | "local" | null; disabled?: boolean; onTryDemo?: () => void; onUseOwnSong?: () => void; @@ -13,6 +14,7 @@ interface EmptyStateProps { /** First-run empty workspace: try the licensed demo or use a local song. */ export function EmptyState({ selectedLabel = null, + selectedKind = null, disabled = false, onTryDemo, onUseOwnSong @@ -28,7 +30,11 @@ export function EmptyState({

{t("workspaceReadyToAnalyzeTitle")}

- {hasSelection ? t("demoSelectedNextAction") : t("workspaceEmptyState")} + {hasSelection + ? selectedKind === "local" + ? t("localSelectedNextAction") + : t("demoSelectedNextAction") + : t("workspaceEmptyState")}

{hasSelection ? null : (

{t("demoLimitation")}

diff --git a/apps/desktop/src/locales/en/common.json b/apps/desktop/src/locales/en/common.json index e8fe6935a..c818e638c 100644 --- a/apps/desktop/src/locales/en/common.json +++ b/apps/desktop/src/locales/en/common.json @@ -152,6 +152,7 @@ "tryTheDemo": "Try the demo", "useMyOwnSong": "Use my own song", "demoSelectedNextAction": "Start analysis to open tonight's first cue.", + "localSelectedNextAction": "Start analysis to open your first cue.", "demoUnavailable": "The licensed demo song could not be loaded. Use your own song to start tonight.", "demoLimitation": "The demo is original BandScope audio for evaluation, not a commercial track.", "chooseDifferentSong": "Choose a different song" diff --git a/apps/desktop/src/locales/ko/common.json b/apps/desktop/src/locales/ko/common.json index 0d313110f..50c4c9627 100644 --- a/apps/desktop/src/locales/ko/common.json +++ b/apps/desktop/src/locales/ko/common.json @@ -152,6 +152,7 @@ "tryTheDemo": "데모 써 보기", "useMyOwnSong": "내 곡 사용", "demoSelectedNextAction": "분석을 시작해 오늘 첫 큐를 여세요.", + "localSelectedNextAction": "분석을 시작해 첫 큐를 여세요.", "demoUnavailable": "라이선스 데모 곡을 불러올 수 없습니다. 내 곡으로 오늘 합주를 시작하세요.", "demoLimitation": "데모는 평가용 오리지널 BandScope 오디오이며 상업 음원이 아닙니다.", "chooseDifferentSong": "다른 곡 선택" From 7c5256fdbd04e97c7239bd728d833b0153a9e1cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 11:38:39 +0900 Subject: [PATCH 17/20] test: remove unused demo selection mock --- apps/desktop/src/App.test.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/desktop/src/App.test.tsx b/apps/desktop/src/App.test.tsx index c6a6ed977..3f6acce4d 100644 --- a/apps/desktop/src/App.test.tsx +++ b/apps/desktop/src/App.test.tsx @@ -19,7 +19,6 @@ const mockLoadProject = vi.fn(); const mockSaveProject = vi.fn(); const mockSubscribeToAnalysisJobUpdates = vi.fn(); let mockLocalAudioSelectionResult: Record | null = null; -let mockDemoAudioSelectionResult: Record | null = null; let mockImportYoutubeUrlError = false; let latestStatusSubscription: ((payload: Record) => void) | null = null; @@ -47,7 +46,7 @@ vi.mock("./lib/analysis", async (importActual) => { roleFocus: ["bass-guitar", "keys-right", "lead-vocal"] }), selectLocalAudioSource: async () => mockLocalAudioSelectionResult ?? actual.selectLocalAudioSource(), - selectDemoAudioSource: async () => mockDemoAudioSelectionResult ?? actual.selectDemoAudioSource(), + selectDemoAudioSource: () => actual.selectDemoAudioSource(), subscribeToAnalysisJobUpdates: (...args: Parameters) => mockSubscribeToAnalysisJobUpdates(...args), loadProject: () => mockLoadProject(), @@ -193,7 +192,6 @@ describe("App", () => { mockSaveProject.mockReset(); mockSubscribeToAnalysisJobUpdates.mockReset(); mockLocalAudioSelectionResult = null; - mockDemoAudioSelectionResult = null; mockImportYoutubeUrlError = false; latestStatusSubscription = null; mockSubscribeToAnalysisJobUpdates.mockImplementation( From 86004787d77cdcfbcc3d411124e9b01ed2898a71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 11:57:03 +0900 Subject: [PATCH 18/20] fix: guard all audio source selection paths --- apps/desktop/src/App.test.tsx | 23 ++++++++++ apps/desktop/src/App.tsx | 44 +++++++++++++------ .../features/workspace/WorkspaceStates.tsx | 4 +- 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/apps/desktop/src/App.test.tsx b/apps/desktop/src/App.test.tsx index 3f6acce4d..09be85022 100644 --- a/apps/desktop/src/App.test.tsx +++ b/apps/desktop/src/App.test.tsx @@ -456,6 +456,28 @@ describe("App", () => { expect(await screen.findByText(/start analysis to open tonight's first cue/i)).toBeTruthy(); }); + it("does not issue duplicate local selection requests while intake is pending", async () => { + let resolveSelection: ((value: unknown) => void) | null = null; + tauriInvoke.mockImplementation( + () => new Promise((resolve) => { + resolveSelection = resolve; + }) + ); + + render(); + + const chooseOwnSong = screen.getByRole("button", { name: /use my own song/i }); + fireEvent.click(chooseOwnSong); + fireEvent.click(chooseOwnSong); + + expect(tauriInvoke).toHaveBeenCalledTimes(1); + await act(async () => { + resolveSelection?.(bootstrapResponse()); + await Promise.resolve(); + }); + expect(await screen.findByText(/start analysis to open your first cue/i)).toBeTruthy(); + }); + it("names using your own song when the licensed demo cannot load", async () => { tauriInvoke.mockRejectedValueOnce( new Error("The licensed demo song could not be loaded. Use your own song to start tonight.") @@ -1172,6 +1194,7 @@ describe("App", () => { url: "https://youtube.com/watch?v=abc123DEF45" }); expect(screen.getByText(/youtube\.wav/i)).toBeTruthy(); + expect(screen.getByText(/start analysis to open your first cue/i)).toBeTruthy(); }); }); diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 1990c5e42..dd2e1eae9 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -259,8 +259,9 @@ export function App() { const [renderedProgressPercent, setRenderedProgressPercent] = useState(undefined); const [isStarting, setIsStarting] = useState(false); const [selectedBootstrap, setSelectedBootstrap] = useState(null); - const [selectedSourceKind, setSelectedSourceKind] = useState<"demo" | "local" | null>(null); + const [selectedSourceKind, setSelectedSourceKind] = useState<"demo" | "local" | "youtube" | null>(null); const [isSelectingDemo, setIsSelectingDemo] = useState(false); + const [isSelectingLocal, setIsSelectingLocal] = useState(false); const [activeAnalysisBootstrap, setActiveAnalysisBootstrap] = useState(null); const [selectionError, setSelectionError] = useState(null); const [selectionErrorSource, setSelectionErrorSource] = useState<"local" | "youtube" | null>(null); @@ -269,6 +270,7 @@ export function App() { const [activeView, setActiveView] = useState("workspace"); const activeJobIdRef = useRef(null); const demoSelectionInFlightRef = useRef(false); + const localSelectionInFlightRef = useRef(false); const youtubeInputRef = useRef(null); const analysisInFlight = jobStatus?.state === "queued" || jobStatus?.state === "running"; @@ -419,24 +421,37 @@ export function App() { /** Documented. */ const handleChooseLocalAudio = async () => { - if (analysisInFlight || isStarting || isImporting || isSelectingDemo) { + if ( + analysisInFlight || + isStarting || + isImporting || + isSelectingDemo || + localSelectionInFlightRef.current + ) { return; } + localSelectionInFlightRef.current = true; + setIsSelectingLocal(true); setSelectionError(null); setSelectionErrorSource(null); - const selection = await selectLocalAudioSource(); - if (selection.ok) { - setSelectedBootstrap(selection.bootstrap); - setSelectedSourceKind("local"); - return; - } + try { + const selection = await selectLocalAudioSource(); + if (selection.ok) { + setSelectedBootstrap(selection.bootstrap); + setSelectedSourceKind("local"); + return; + } - setSelectedBootstrap(null); - setSelectedSourceKind(null); - setSelectionError(safeErrorDetail(selection.error.message, t("unsupportedLocalAudio"))); - setSelectionErrorSource("local"); - setJobStatus(null); + setSelectedBootstrap(null); + setSelectedSourceKind(null); + setSelectionError(safeErrorDetail(selection.error.message, t("unsupportedLocalAudio"))); + setSelectionErrorSource("local"); + setJobStatus(null); + } finally { + localSelectionInFlightRef.current = false; + setIsSelectingLocal(false); + } }; /** Validate the bundled licensed demo through the same local-audio bootstrap. */ @@ -490,6 +505,7 @@ export function App() { const selection = await importYoutubeUrl(normalizedUrl); if (selection.ok) { setSelectedBootstrap(selection.bootstrap); + setSelectedSourceKind("youtube"); setYoutubeUrl(""); } else { setSelectionError(safeErrorDetail(selection.error.message, t("youtubeImportFailed"))); @@ -557,7 +573,7 @@ export function App() { diff --git a/apps/desktop/src/features/workspace/WorkspaceStates.tsx b/apps/desktop/src/features/workspace/WorkspaceStates.tsx index f24507d06..796e9ac24 100644 --- a/apps/desktop/src/features/workspace/WorkspaceStates.tsx +++ b/apps/desktop/src/features/workspace/WorkspaceStates.tsx @@ -5,7 +5,7 @@ import { Loader2, Music, AlertCircle } from "lucide-react"; interface EmptyStateProps { selectedLabel?: string | null; - selectedKind?: "demo" | "local" | null; + selectedKind?: "demo" | "local" | "youtube" | null; disabled?: boolean; onTryDemo?: () => void; onUseOwnSong?: () => void; @@ -31,7 +31,7 @@ export function EmptyState({

{t("workspaceReadyToAnalyzeTitle")}

{hasSelection - ? selectedKind === "local" + ? selectedKind === "local" || selectedKind === "youtube" ? t("localSelectedNextAction") : t("demoSelectedNextAction") : t("workspaceEmptyState")} From 2adaf85c92636a43b2ee7b2a7e7653ac2fa4a3aa Mon Sep 17 00:00:00 2001 From: seonghobae Date: Wed, 26 Aug 2026 21:26:16 +0900 Subject: [PATCH 19/20] fix(app): disable source-bar intake buttons while a selection is pending The persistent Choose local audio button (and the adjacent import control) stayed enabled during a pending demo/local selection, showing no pending feedback; handlers already early-return via localSelectionInFlightRef so this is feedback parity with the EmptyState buttons flagged by review. Evidence: App.test 68/68 incl. new pending-disabled assertion. --- apps/desktop/src/App.test.tsx | 15 +++++++++++++++ apps/desktop/src/App.tsx | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/App.test.tsx b/apps/desktop/src/App.test.tsx index 09be85022..761690cab 100644 --- a/apps/desktop/src/App.test.tsx +++ b/apps/desktop/src/App.test.tsx @@ -506,6 +506,21 @@ describe("App", () => { expect(screen.queryByText(/analysis failed during execution/i)).toBeNull(); }); + it("disables the source-bar local-audio button while a selection is pending", async () => { + let resolveSelection!: (value: unknown) => void; + mockLocalAudioSelectionResult = new Promise((resolve) => { + resolveSelection = resolve; + }); + render(); + + const sourceBarButton = screen.getByRole("button", { name: "Choose local audio" }); + fireEvent.click(sourceBarButton); + await waitFor(() => expect(sourceBarButton).toBeDisabled()); + + resolveSelection({ ok: false, error: { code: "invalid_request", message: "" } }); + await waitFor(() => expect(sourceBarButton).toBeEnabled()); + }); + it("falls back to generic local-audio error copy when selection omits a message", async () => { mockLocalAudioSelectionResult = { ok: false, diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index dd2e1eae9..4ce64ad53 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -745,7 +745,7 @@ export function App() {