From 9b0067b4c061dabb4e1c07e090c150f0a1b2fa9d Mon Sep 17 00:00:00 2001 From: Jake Date: Thu, 23 Apr 2026 00:16:09 +0100 Subject: [PATCH] Land release blocker fixes and workspace cleanup --- crates/ai-formatting/src/llm_client.rs | 15 +- crates/ai-formatting/src/rule_based.rs | 8 +- crates/ai-formatting/src/to_plain_text.rs | 5 +- crates/audio/src/wav.rs | 14 +- crates/cloud-providers/src/keystore.rs | 84 +- crates/hotkey/src/linux.rs | 5 +- crates/llm/src/lib.rs | 55 +- crates/mcp/src/lib.rs | 25 +- crates/mcp/src/main.rs | 5 +- crates/storage/src/database.rs | 117 ++- crates/storage/src/migrations.rs | 275 +++++- crates/transcription/src/lib.rs | 4 +- crates/transcription/src/model_manager.rs | 5 +- .../src/streaming/commit_policy.rs | 6 +- crates/transcription/src/streaming/rms_vad.rs | 7 +- .../transcription/src/whisper_rs_backend.rs | 21 +- docs/issues/README.md | 35 +- docs/issues/c1-live-session-race.md | 27 +- docs/issues/c4-transcript-profile-fk.md | 27 + docs/issues/keystore-thread-safety.md | 22 + docs/issues/llm-prompt-preflight.md | 19 + .../issues/poll-inference-channel-fatality.md | 25 + docs/issues/power-assertion-macos-objc2.md | 27 + docs/issues/run-live-session-monolith.md | 27 + src-tauri/Cargo.toml | 4 + src-tauri/build.rs | 4 +- src-tauri/gen/schemas/acl-manifests.json | 2 +- src-tauri/gen/schemas/desktop-schema.json | 20 +- src-tauri/gen/schemas/linux-schema.json | 20 +- src-tauri/src/commands/audio.rs | 19 +- src-tauri/src/commands/diagnostics.rs | 15 + src-tauri/src/commands/live.rs | 830 +++++++++++++----- src-tauri/src/commands/models.rs | 20 +- src-tauri/src/commands/paste.rs | 17 +- src-tauri/src/commands/power.rs | 130 ++- src-tauri/src/lib.rs | 6 +- 36 files changed, 1529 insertions(+), 418 deletions(-) diff --git a/crates/ai-formatting/src/llm_client.rs b/crates/ai-formatting/src/llm_client.rs index 1612832..e2a446e 100644 --- a/crates/ai-formatting/src/llm_client.rs +++ b/crates/ai-formatting/src/llm_client.rs @@ -225,13 +225,19 @@ mod tests { assert_eq!(LlmPromptPreset::parse("EMAIL"), LlmPromptPreset::Email); assert_eq!(LlmPromptPreset::parse("notes"), LlmPromptPreset::Notes); assert_eq!(LlmPromptPreset::parse("meeting"), LlmPromptPreset::Notes); - assert_eq!(LlmPromptPreset::parse("meeting-notes"), LlmPromptPreset::Notes); + assert_eq!( + LlmPromptPreset::parse("meeting-notes"), + LlmPromptPreset::Notes + ); assert_eq!(LlmPromptPreset::parse("code"), LlmPromptPreset::Code); assert_eq!(LlmPromptPreset::parse("software"), LlmPromptPreset::Code); // Unknown values and explicit default fall back safely. assert_eq!(LlmPromptPreset::parse("default"), LlmPromptPreset::Default); assert_eq!(LlmPromptPreset::parse(""), LlmPromptPreset::Default); - assert_eq!(LlmPromptPreset::parse("random-unknown"), LlmPromptPreset::Default); + assert_eq!( + LlmPromptPreset::parse("random-unknown"), + LlmPromptPreset::Default + ); } #[test] @@ -240,7 +246,10 @@ mod tests { // be empty so it composes cleanly with dictionary suffix. assert!(LlmPromptPreset::Default.suffix().is_empty()); assert!(LlmPromptPreset::Email.suffix().contains("email")); - assert!(LlmPromptPreset::Notes.suffix().to_lowercase().contains("bullet")); + assert!(LlmPromptPreset::Notes + .suffix() + .to_lowercase() + .contains("bullet")); assert!(LlmPromptPreset::Code.suffix().contains("technical")); } } diff --git a/crates/ai-formatting/src/rule_based.rs b/crates/ai-formatting/src/rule_based.rs index 742bf9b..1307c3b 100644 --- a/crates/ai-formatting/src/rule_based.rs +++ b/crates/ai-formatting/src/rule_based.rs @@ -540,8 +540,12 @@ mod tests { fn is_hallucination_allows_dialogue_containing_thanks_mid_sentence() { // Exact-match on trail phrases means legitimate dialogue that // mentions "thanks" or "subscribe" is never dropped. - assert!(!is_hallucination("Thanks for the heads up on the migration")); - assert!(!is_hallucination("Please subscribe to the RSS feed and tell me when it updates")); + assert!(!is_hallucination( + "Thanks for the heads up on the migration" + )); + assert!(!is_hallucination( + "Please subscribe to the RSS feed and tell me when it updates" + )); } #[test] diff --git a/crates/ai-formatting/src/to_plain_text.rs b/crates/ai-formatting/src/to_plain_text.rs index 53e9f7d..3a193c8 100644 --- a/crates/ai-formatting/src/to_plain_text.rs +++ b/crates/ai-formatting/src/to_plain_text.rs @@ -168,7 +168,10 @@ mod tests { ]; for (input, expected) in cases { let out = to_plain_text(&[seg(input)]); - assert_eq!(out, expected, "input {input:?} should strip to {expected:?}"); + assert_eq!( + out, expected, + "input {input:?} should strip to {expected:?}" + ); } } diff --git a/crates/audio/src/wav.rs b/crates/audio/src/wav.rs index 1872f69..1ca1801 100644 --- a/crates/audio/src/wav.rs +++ b/crates/audio/src/wav.rs @@ -42,9 +42,8 @@ impl WavWriter { }; let file = std::fs::File::create(path).map_err(KonError::Io)?; let buffered = BufWriter::new(file); - let inner = hound::WavWriter::new(buffered, spec).map_err(|e| { - KonError::Io(std::io::Error::other(format!("WAV create failed: {e}"))) - })?; + let inner = hound::WavWriter::new(buffered, spec) + .map_err(|e| KonError::Io(std::io::Error::other(format!("WAV create failed: {e}"))))?; Ok(Self { inner, samples_since_flush: 0, @@ -77,9 +76,9 @@ impl WavWriter { /// `Self::DEFAULT_FLUSH_EVERY_SAMPLES` — but may do so at natural /// boundaries (end-of-utterance, UI events) for tighter recovery. pub fn flush(&mut self) -> Result<()> { - self.inner.flush().map_err(|e| { - KonError::Io(std::io::Error::other(format!("WAV flush failed: {e}"))) - })?; + self.inner + .flush() + .map_err(|e| KonError::Io(std::io::Error::other(format!("WAV flush failed: {e}"))))?; self.samples_since_flush = 0; Ok(()) } @@ -244,8 +243,7 @@ mod tests { let _ = std::fs::remove_file(&path); // Write 100 samples (200 bytes at 16-bit). - let original = - AudioSamples::mono_16khz((0..100).map(|i| (i as f32) / 100.0).collect()); + let original = AudioSamples::mono_16khz((0..100).map(|i| (i as f32) / 100.0).collect()); write_wav(&path, &original).unwrap(); // Drop the last 10 bytes — 5 samples' worth. hound's iterator diff --git a/crates/cloud-providers/src/keystore.rs b/crates/cloud-providers/src/keystore.rs index 871565c..e4ef053 100644 --- a/crates/cloud-providers/src/keystore.rs +++ b/crates/cloud-providers/src/keystore.rs @@ -1,29 +1,77 @@ -/// Store an API key in the OS keychain. +use std::collections::HashMap; +use std::sync::{Mutex, OnceLock}; + +/// Store an API key in Kon's process-local keystore. /// -/// Stub implementation using environment variables until the `keyring` crate is -/// added. Keys are only held in-process and lost on exit. +/// Keys are held in memory for the lifetime of the process and are lost on +/// exit. This avoids the undefined behaviour of mutating process environment +/// variables from arbitrary threads while keeping the public API safe. /// -/// # Safety note -/// `std::env::set_var` is deprecated in Rust 2024 edition and is **not** -/// thread-safe — mutating the environment while other threads read it is -/// undefined behaviour. This is acceptable during single-threaded app init -/// but must not be called from async/multi-threaded contexts. +/// `retrieve_api_key` still falls back to `KON_API_KEY_` environment +/// variables so externally injected secrets continue to work. /// /// TODO: Replace with the `keyring` crate (or platform-native credential /// storage) so keys persist across sessions and are accessed safely. -#[allow(deprecated)] // set_var deprecated in Rust 2024 edition pub fn store_api_key(provider: &str, key: &str) { - // SAFETY: Only safe when called from a single-threaded context (e.g. app - // initialisation). See doc comment above. - std::env::set_var(format!("KON_API_KEY_{}", provider.to_uppercase()), key); + api_key_store() + .lock() + .unwrap() + .insert(provider_env_key(provider), key.to_string()); } -/// Retrieve an API key from the OS keychain. +/// Retrieve an API key from Kon's process-local keystore. /// -/// Stub implementation using environment variables until the `keyring` crate is -/// added. Returns `None` if no key has been stored this session. -/// -/// TODO: Replace with the `keyring` crate alongside `store_api_key`. +/// Returns a previously stored in-memory key when present, otherwise falls +/// back to the read-only `KON_API_KEY_` environment variable so +/// operator-supplied secrets still work. pub fn retrieve_api_key(provider: &str) -> Option { - std::env::var(format!("KON_API_KEY_{}", provider.to_uppercase())).ok() + let env_key = provider_env_key(provider); + api_key_store() + .lock() + .unwrap() + .get(&env_key) + .cloned() + .or_else(|| std::env::var(env_key).ok()) +} + +fn api_key_store() -> &'static Mutex> { + static STORE: OnceLock>> = OnceLock::new(); + STORE.get_or_init(|| Mutex::new(HashMap::new())) +} + +fn provider_env_key(provider: &str) -> String { + format!("KON_API_KEY_{}", provider.to_uppercase()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicUsize, Ordering}; + + fn unique_provider(prefix: &str) -> String { + static NEXT_ID: AtomicUsize = AtomicUsize::new(1); + format!("{prefix}_{}", NEXT_ID.fetch_add(1, Ordering::Relaxed)) + } + + #[test] + fn stored_key_is_retrievable_without_env_mutation() { + let provider = unique_provider("provider"); + store_api_key(&provider, "secret-token"); + assert_eq!( + retrieve_api_key(&provider), + Some("secret-token".to_string()) + ); + } + + #[test] + fn providers_do_not_overlap() { + let first = unique_provider("first"); + let second = unique_provider("second"); + + store_api_key(&first, "alpha"); + store_api_key(&second, "beta"); + + assert_eq!(retrieve_api_key(&first), Some("alpha".to_string())); + assert_eq!(retrieve_api_key(&second), Some("beta".to_string())); + } } diff --git a/crates/hotkey/src/linux.rs b/crates/hotkey/src/linux.rs index cc133f1..5a5febf 100644 --- a/crates/hotkey/src/linux.rs +++ b/crates/hotkey/src/linux.rs @@ -349,10 +349,7 @@ fn is_event_device(path: &Path) -> bool { /// Return true when the device's reported key set includes the combo's /// configured trigger key. A device that reports no keys at all (for /// example a mouse whose `EV_KEY` capability is buttons only) is rejected. -fn device_supports_combo( - supported: Option<&AttributeSetRef>, - combo: &HotkeyCombo, -) -> bool { +fn device_supports_combo(supported: Option<&AttributeSetRef>, combo: &HotkeyCombo) -> bool { supported.map_or(false, |keys| keys.contains(Key::new(combo.key_code))) } diff --git a/crates/llm/src/lib.rs b/crates/llm/src/lib.rs index 213f2c5..1008610 100644 --- a/crates/llm/src/lib.rs +++ b/crates/llm/src/lib.rs @@ -18,6 +18,8 @@ pub mod prompts; pub use model_manager::{recommend_tier, LlmModelId, LlmModelInfo}; const DEFAULT_CONTEXT_TOKENS: u32 = 4096; +const MAX_CONTEXT_TOKENS: u32 = 8192; +const CONTEXT_RESERVE_TOKENS: u32 = 64; const GENERATION_SEED: u32 = 0; #[derive(Debug, thiserror::Error)] @@ -26,6 +28,15 @@ pub enum EngineError { NotLoaded, #[error("LLM load failed: {0}")] LoadFailed(String), + #[error( + "prompt too long: {prompt_tokens} prompt tokens exceed the {available_prompt_tokens}-token prompt budget for an {context_window}-token context with {max_tokens} reserved response tokens" + )] + PromptTooLong { + prompt_tokens: usize, + max_tokens: u32, + available_prompt_tokens: u32, + context_window: u32, + }, #[error("inference failed: {0}")] Inference(String), #[error("model output not valid JSON: {0}")] @@ -149,7 +160,7 @@ impl LlmEngine { return Ok(String::new()); } - let n_ctx = context_window_size(prompt_tokens.len(), config.max_tokens); + let n_ctx = preflight_context_window(prompt_tokens.len(), config.max_tokens)?; let thread_count = i32::try_from(num_cpus::get().max(1)).unwrap_or(4); let ctx_params = LlamaContextParams::default() .with_n_ctx(Some( @@ -317,8 +328,26 @@ impl LlmEngine { fn context_window_size(prompt_tokens: usize, max_tokens: u32) -> u32 { let required = prompt_tokens .saturating_add(max_tokens as usize) - .saturating_add(64); - DEFAULT_CONTEXT_TOKENS.max(required.min(8192) as u32) + .saturating_add(CONTEXT_RESERVE_TOKENS as usize); + DEFAULT_CONTEXT_TOKENS.max(required.min(MAX_CONTEXT_TOKENS as usize) as u32) +} + +fn preflight_context_window(prompt_tokens: usize, max_tokens: u32) -> Result { + let required = prompt_tokens + .saturating_add(max_tokens as usize) + .saturating_add(CONTEXT_RESERVE_TOKENS as usize); + if required > MAX_CONTEXT_TOKENS as usize { + let available_prompt_tokens = + MAX_CONTEXT_TOKENS.saturating_sub(max_tokens.saturating_add(CONTEXT_RESERVE_TOKENS)); + return Err(EngineError::PromptTooLong { + prompt_tokens, + max_tokens, + available_prompt_tokens, + context_window: MAX_CONTEXT_TOKENS, + }); + } + + Ok(context_window_size(prompt_tokens, max_tokens)) } fn first_stop_index(text: &str, stop_sequences: &[String]) -> Option { @@ -417,4 +446,24 @@ mod tests { let index = first_stop_index(text, &["<|im_end|>".into(), "zzz".into()]); assert_eq!(index, Some(5)); } + + #[test] + fn prompt_preflight_rejects_oversized_prompt_tokens() { + let err = preflight_context_window(7_105, 1_024).unwrap_err(); + assert!(matches!( + err, + EngineError::PromptTooLong { + prompt_tokens: 7_105, + max_tokens: 1_024, + available_prompt_tokens: 7_104, + context_window: MAX_CONTEXT_TOKENS, + } + )); + } + + #[test] + fn prompt_preflight_keeps_prompts_within_budget() { + let n_ctx = preflight_context_window(7_104, 1_024).unwrap(); + assert_eq!(n_ctx, MAX_CONTEXT_TOKENS); + } } diff --git a/crates/mcp/src/lib.rs b/crates/mcp/src/lib.rs index 34527ff..5ecd158 100644 --- a/crates/mcp/src/lib.rs +++ b/crates/mcp/src/lib.rs @@ -226,7 +226,9 @@ async fn list_transcripts_tool(pool: &SqlitePool, args: Value) -> Result Result { @@ -288,7 +290,9 @@ async fn search_transcripts_tool(pool: &SqlitePool, args: Value) -> Result Result { @@ -311,7 +315,9 @@ async fn list_tasks_tool(pool: &SqlitePool) -> Result { }) .collect(); - Ok(text_content(serde_json::to_string_pretty(&summaries).unwrap())) + Ok(text_content( + serde_json::to_string_pretty(&summaries).unwrap(), + )) } fn text_content(text: String) -> Value { @@ -404,7 +410,10 @@ mod tests { let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap(); let response = handle_message(&pool, request).await.expect("has response"); - let tools = response.result.expect("ok")["tools"].as_array().unwrap().clone(); + let tools = response.result.expect("ok")["tools"] + .as_array() + .unwrap() + .clone(); let names: Vec = tools .iter() .map(|tool| tool["name"].as_str().unwrap().to_string()) @@ -426,7 +435,9 @@ mod tests { assert_eq!(resp.jsonrpc, "2.0"); assert_eq!(resp.id, Value::Null); assert!(resp.result.is_none()); - let err = resp.error.expect("parse_error_response must carry an error"); + let err = resp + .error + .expect("parse_error_response must carry an error"); assert_eq!(err.code, -32700); assert!(err.message.contains("Parse error")); assert!(err.message.contains("expected value")); @@ -449,7 +460,9 @@ mod tests { }); let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap(); - kon_storage::migrations::run_migrations(&pool).await.unwrap(); + kon_storage::migrations::run_migrations(&pool) + .await + .unwrap(); let response = handle_message(&pool, request).await.expect("has response"); assert!( diff --git a/crates/mcp/src/main.rs b/crates/mcp/src/main.rs index 31e75d9..a970eec 100644 --- a/crates/mcp/src/main.rs +++ b/crates/mcp/src/main.rs @@ -7,10 +7,7 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; #[tokio::main(flavor = "current_thread")] async fn main() -> anyhow::Result<()> { let db_path = kon_storage::database_path(); - eprintln!( - "[kon-mcp] opening Kon database at {}", - db_path.display() - ); + eprintln!("[kon-mcp] opening Kon database at {}", db_path.display()); let pool = kon_storage::init(&db_path).await?; eprintln!("[kon-mcp] ready, waiting for JSON-RPC on stdin"); diff --git a/crates/storage/src/database.rs b/crates/storage/src/database.rs index eaae161..2aacd89 100644 --- a/crates/storage/src/database.rs +++ b/crates/storage/src/database.rs @@ -62,6 +62,13 @@ pub async fn insert_transcript( pool: &SqlitePool, params: &InsertTranscriptParams<'_>, ) -> Result<()> { + if !profile_exists(pool, params.profile_id).await? { + return Err(KonError::StorageError(format!( + "Insert transcript failed: unknown profile id '{}'", + params.profile_id + ))); + } + sqlx::query( "INSERT INTO transcripts (id, text, source, profile_id, title, audio_path, duration, engine, model_id, inference_ms, sample_rate, audio_channels, format_mode, remove_fillers, british_english, anti_hallucination) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", @@ -731,6 +738,12 @@ pub async fn delete_profile(pool: &SqlitePool, id: &str) -> Result<()> { "Default profile cannot be deleted".into(), )); } + let transcript_count = transcript_count_for_profile(pool, id).await?; + if transcript_count > 0 { + return Err(KonError::StorageError(format!( + "Cannot delete profile while {transcript_count} transcript(s) still reference it; reassign transcripts first" + ))); + } sqlx::query("DELETE FROM profiles WHERE id = ?") .bind(id) .execute(pool) @@ -789,6 +802,23 @@ pub async fn delete_profile_term(pool: &SqlitePool, id: &str) -> Result<()> { Ok(()) } +async fn profile_exists(pool: &SqlitePool, id: &str) -> Result { + let exists: Option = sqlx::query_scalar("SELECT 1 FROM profiles WHERE id = ? LIMIT 1") + .bind(id) + .fetch_optional(pool) + .await + .map_err(|e| KonError::StorageError(format!("Profile existence check failed: {e}")))?; + Ok(exists.is_some()) +} + +async fn transcript_count_for_profile(pool: &SqlitePool, id: &str) -> Result { + sqlx::query_scalar("SELECT COUNT(*) FROM transcripts WHERE profile_id = ?") + .bind(id) + .fetch_one(pool) + .await + .map_err(|e| KonError::StorageError(format!("Profile transcript count failed: {e}"))) +} + // --- Error Logging --- /// Log a structured error to the `error_log` table. @@ -865,9 +895,14 @@ mod tests { async fn test_pool() -> SqlitePool { let pool = SqlitePoolOptions::new() + .max_connections(1) .connect("sqlite::memory:") .await .unwrap(); + sqlx::query("PRAGMA foreign_keys = ON") + .execute(&pool) + .await + .unwrap(); run_migrations(&pool).await.unwrap(); pool } @@ -1047,11 +1082,19 @@ mod tests { insert_task(&pool, "p1", "Ship release", "inbox", None, None, None) .await .unwrap(); - insert_subtask(&pool, "s1", "Final test", "p1").await.unwrap(); - insert_subtask(&pool, "s2", "Tag release", "p1").await.unwrap(); + insert_subtask(&pool, "s1", "Final test", "p1") + .await + .unwrap(); + insert_subtask(&pool, "s2", "Tag release", "p1") + .await + .unwrap(); - complete_subtask_and_check_parent(&pool, "s1").await.unwrap(); - complete_subtask_and_check_parent(&pool, "s2").await.unwrap(); + complete_subtask_and_check_parent(&pool, "s1") + .await + .unwrap(); + complete_subtask_and_check_parent(&pool, "s2") + .await + .unwrap(); let parent = get_task_by_id(&pool, "p1").await.unwrap().unwrap(); assert!(parent.done); @@ -1353,6 +1396,72 @@ mod tests { assert_eq!(profiles.len(), 1); } + #[tokio::test] + async fn insert_transcript_rejects_unknown_profile_id() { + let pool = test_pool().await; + let err = insert_transcript( + &pool, + &InsertTranscriptParams { + id: "bad-profile", + text: "Hello", + source: "microphone", + profile_id: "profile-missing", + title: None, + audio_path: None, + duration: 0.0, + engine: None, + model_id: None, + inference_ms: None, + sample_rate: None, + audio_channels: None, + format_mode: None, + remove_fillers: false, + british_english: false, + anti_hallucination: false, + }, + ) + .await + .expect_err("unknown profile id must be rejected"); + + let msg = err.to_string(); + assert!(msg.contains("unknown profile id"), "got: {msg}"); + } + + #[tokio::test] + async fn delete_profile_rejects_when_transcripts_reference_it() { + let pool = test_pool().await; + let profile = create_profile(&pool, "Referenced", "").await.unwrap(); + insert_transcript( + &pool, + &InsertTranscriptParams { + id: "referenced-transcript", + text: "Hello", + source: "microphone", + profile_id: &profile.id, + title: None, + audio_path: None, + duration: 0.0, + engine: None, + model_id: None, + inference_ms: None, + sample_rate: None, + audio_channels: None, + format_mode: None, + remove_fillers: false, + british_english: false, + anti_hallucination: false, + }, + ) + .await + .unwrap(); + + let err = delete_profile(&pool, &profile.id) + .await + .expect_err("profile with transcript references must not delete"); + let msg = err.to_string(); + assert!(msg.contains("reassign transcripts first"), "got: {msg}"); + } + #[tokio::test] async fn list_profile_terms_happy_path() { let pool = test_pool().await; diff --git a/crates/storage/src/migrations.rs b/crates/storage/src/migrations.rs index 18a3ed9..1e26833 100644 --- a/crates/storage/src/migrations.rs +++ b/crates/storage/src/migrations.rs @@ -215,6 +215,125 @@ const MIGRATIONS: &[(i64, &str, &str)] = &[ ON transcripts(profile_id); "#, ), + ( + 9, + "transcript_profile_fk", + r#" + INSERT OR IGNORE INTO profiles (id, name, initial_prompt, created_at) + VALUES ('00000000-0000-0000-0000-000000000001', 'Default', '', datetime('now')); + + DROP TRIGGER IF EXISTS transcripts_ai; + DROP TRIGGER IF EXISTS transcripts_ad; + DROP TRIGGER IF EXISTS transcripts_au; + DROP TABLE IF EXISTS transcripts_fts; + DROP INDEX IF EXISTS idx_segments_transcript; + DROP INDEX IF EXISTS idx_transcripts_created; + DROP INDEX IF EXISTS idx_transcripts_profile_id; + + ALTER TABLE segments RENAME TO segments_old; + ALTER TABLE transcripts RENAME TO transcripts_old; + + CREATE TABLE transcripts ( + id TEXT PRIMARY KEY, + text TEXT NOT NULL DEFAULT '', + source TEXT NOT NULL DEFAULT 'microphone', + title TEXT, + audio_path TEXT, + duration REAL NOT NULL DEFAULT 0.0, + engine TEXT, + model_id TEXT, + inference_ms INTEGER, + sample_rate INTEGER, + audio_channels INTEGER, + format_mode TEXT, + remove_fillers INTEGER NOT NULL DEFAULT 0, + british_english INTEGER NOT NULL DEFAULT 0, + anti_hallucination INTEGER NOT NULL DEFAULT 0, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + starred INTEGER NOT NULL DEFAULT 0, + manual_tags TEXT NOT NULL DEFAULT '', + template TEXT NOT NULL DEFAULT '', + language TEXT NOT NULL DEFAULT '', + segments_json TEXT NOT NULL DEFAULT '', + profile_id TEXT NOT NULL DEFAULT '00000000-0000-0000-0000-000000000001' + REFERENCES profiles(id) ON DELETE RESTRICT + ); + + CREATE INDEX idx_transcripts_created + ON transcripts(created_at); + CREATE INDEX idx_transcripts_profile_id + ON transcripts(profile_id); + + INSERT INTO transcripts ( + id, text, source, title, audio_path, duration, engine, model_id, + inference_ms, sample_rate, audio_channels, format_mode, + remove_fillers, british_english, anti_hallucination, created_at, + starred, manual_tags, template, language, segments_json, profile_id + ) + SELECT + id, text, source, title, audio_path, duration, engine, model_id, + inference_ms, sample_rate, audio_channels, format_mode, + remove_fillers, british_english, anti_hallucination, created_at, + starred, manual_tags, template, language, segments_json, + CASE + WHEN profile_id IS NOT NULL + AND EXISTS ( + SELECT 1 FROM profiles + WHERE id = transcripts_old.profile_id + ) + THEN profile_id + ELSE '00000000-0000-0000-0000-000000000001' + END + FROM transcripts_old; + + CREATE TABLE segments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + transcript_id TEXT NOT NULL REFERENCES transcripts(id) ON DELETE CASCADE, + start_time REAL NOT NULL, + end_time REAL NOT NULL, + text TEXT NOT NULL DEFAULT '' + ); + + CREATE INDEX idx_segments_transcript + ON segments(transcript_id); + + INSERT INTO segments (id, transcript_id, start_time, end_time, text) + SELECT id, transcript_id, start_time, end_time, text + FROM segments_old; + + DROP TABLE segments_old; + DROP TABLE transcripts_old; + + CREATE VIRTUAL TABLE transcripts_fts USING fts5( + text, + title, + content='transcripts', + content_rowid='rowid', + tokenize='porter unicode61 remove_diacritics 2' + ); + + CREATE TRIGGER transcripts_ai AFTER INSERT ON transcripts BEGIN + INSERT INTO transcripts_fts(rowid, text, title) + VALUES (new.rowid, new.text, COALESCE(new.title, '')); + END; + + CREATE TRIGGER transcripts_ad AFTER DELETE ON transcripts BEGIN + INSERT INTO transcripts_fts(transcripts_fts, rowid, text, title) + VALUES ('delete', old.rowid, old.text, COALESCE(old.title, '')); + END; + + CREATE TRIGGER transcripts_au AFTER UPDATE ON transcripts BEGIN + INSERT INTO transcripts_fts(transcripts_fts, rowid, text, title) + VALUES ('delete', old.rowid, old.text, COALESCE(old.title, '')); + INSERT INTO transcripts_fts(rowid, text, title) + VALUES (new.rowid, new.text, COALESCE(new.title, '')); + END; + + INSERT INTO transcripts_fts(rowid, text, title) + SELECT rowid, text, COALESCE(title, '') + FROM transcripts; + "#, + ), ]; /// Split SQL into individual statements, respecting BEGIN...END trigger blocks. @@ -281,10 +400,7 @@ pub async fn run_migrations(pool: &SqlitePool) -> Result<()> { /// implicitly commits (`VACUUM`, `REINDEX`, `ATTACH`), it must be split /// out into its own non-transactional migration — reviewer's job to /// flag. -async fn run_migrations_slice( - pool: &SqlitePool, - migrations: &[(i64, &str, &str)], -) -> Result<()> { +async fn run_migrations_slice(pool: &SqlitePool, migrations: &[(i64, &str, &str)]) -> Result<()> { sqlx::query( "CREATE TABLE IF NOT EXISTS schema_version ( version INTEGER PRIMARY KEY, @@ -344,12 +460,22 @@ mod tests { use sqlx::sqlite::SqlitePoolOptions; use sqlx::Row; - #[tokio::test] - async fn test_migrations_run_on_empty_db() { + async fn fk_test_pool() -> SqlitePool { let pool = SqlitePoolOptions::new() + .max_connections(1) .connect("sqlite::memory:") .await - .unwrap(); + .expect("pool"); + sqlx::query("PRAGMA foreign_keys = ON") + .execute(&pool) + .await + .expect("enable foreign keys"); + pool + } + + #[tokio::test] + async fn test_migrations_run_on_empty_db() { + let pool = fk_test_pool().await; run_migrations(&pool).await.unwrap(); @@ -357,7 +483,7 @@ mod tests { .fetch_one(&pool) .await .unwrap(); - assert_eq!(count, 8); + assert_eq!(count, 9); sqlx::query("INSERT INTO settings (key, value) VALUES ('test', 'value')") .execute(&pool) @@ -367,10 +493,7 @@ mod tests { #[tokio::test] async fn test_migrations_idempotent() { - let pool = SqlitePoolOptions::new() - .connect("sqlite::memory:") - .await - .unwrap(); + let pool = fk_test_pool().await; run_migrations(&pool).await.unwrap(); run_migrations(&pool).await.unwrap(); @@ -379,7 +502,7 @@ mod tests { .fetch_one(&pool) .await .unwrap(); - assert_eq!(count, 8); + assert_eq!(count, 9); } #[tokio::test] @@ -440,11 +563,7 @@ mod tests { #[tokio::test] async fn migration_transcript_profile_provenance_adds_profile_id() { - let pool = SqlitePoolOptions::new() - .max_connections(1) - .connect("sqlite::memory:") - .await - .expect("pool"); + let pool = fk_test_pool().await; run_migrations(&pool).await.expect("migrate"); let info = sqlx::query("PRAGMA table_info(transcripts)") @@ -459,11 +578,108 @@ mod tests { } #[tokio::test] - async fn test_parent_task_id_cascade_delete() { - let pool = SqlitePoolOptions::new() - .connect("sqlite::memory:") + async fn migration_v9_reconciles_orphaned_transcript_profiles_and_adds_fk() { + let pool = fk_test_pool().await; + run_migrations_up_to(&pool, 8).await.expect("migrate to v8"); + + sqlx::query( + "INSERT INTO profiles (id, name, initial_prompt, created_at) + VALUES ('profile-valid', 'Valid', '', datetime('now'))", + ) + .execute(&pool) + .await + .expect("seed valid profile"); + + sqlx::query( + "INSERT INTO transcripts ( + id, text, source, title, audio_path, duration, engine, model_id, + inference_ms, sample_rate, audio_channels, format_mode, + remove_fillers, british_english, anti_hallucination, created_at, + starred, manual_tags, template, language, segments_json, profile_id + ) VALUES ( + 't-orphan', 'orphan body', 'microphone', 'Orphan', NULL, 0.0, NULL, NULL, + NULL, NULL, NULL, NULL, 0, 0, 0, datetime('now'), + 0, '', '', '', '', 'profile-missing' + )", + ) + .execute(&pool) + .await + .expect("seed orphan transcript"); + sqlx::query( + "INSERT INTO transcripts ( + id, text, source, title, audio_path, duration, engine, model_id, + inference_ms, sample_rate, audio_channels, format_mode, + remove_fillers, british_english, anti_hallucination, created_at, + starred, manual_tags, template, language, segments_json, profile_id + ) VALUES ( + 't-valid', 'valid body', 'microphone', 'Valid', NULL, 0.0, NULL, NULL, + NULL, NULL, NULL, NULL, 0, 0, 0, datetime('now'), + 0, '', '', '', '', 'profile-valid' + )", + ) + .execute(&pool) + .await + .expect("seed valid transcript"); + sqlx::query( + "INSERT INTO segments (transcript_id, start_time, end_time, text) + VALUES ('t-orphan', 0.0, 1.0, 'segment')", + ) + .execute(&pool) + .await + .expect("seed segment"); + + run_migrations(&pool).await.expect("migrate to v9"); + + let orphan_profile: String = + sqlx::query_scalar("SELECT profile_id FROM transcripts WHERE id = 't-orphan'") + .fetch_one(&pool) + .await + .expect("read healed orphan"); + assert_eq!(orphan_profile, crate::DEFAULT_PROFILE_ID); + + let valid_profile: String = + sqlx::query_scalar("SELECT profile_id FROM transcripts WHERE id = 't-valid'") + .fetch_one(&pool) + .await + .expect("read preserved profile"); + assert_eq!(valid_profile, "profile-valid"); + + let segment_count: i64 = + sqlx::query_scalar("SELECT COUNT(*) FROM segments WHERE transcript_id = 't-orphan'") + .fetch_one(&pool) + .await + .expect("read migrated segments"); + assert_eq!(segment_count, 1, "segments must survive transcript rebuild"); + + let fk_rows = sqlx::query("PRAGMA foreign_key_list(transcripts)") + .fetch_all(&pool) .await - .unwrap(); + .expect("read transcript foreign keys"); + assert!( + fk_rows.iter().any(|row| { + row.get::("table") == "profiles" + && row.get::("from") == "profile_id" + && row.get::("to") == "id" + && row.get::("on_delete") == "RESTRICT" + }), + "transcripts.profile_id must reference profiles(id) with ON DELETE RESTRICT" + ); + + let fts_hits: i64 = sqlx::query_scalar( + "SELECT COUNT(*) FROM transcripts_fts WHERE transcripts_fts MATCH 'orphan'", + ) + .fetch_one(&pool) + .await + .expect("query rebuilt fts"); + assert_eq!( + fts_hits, 1, + "fts index must be rebuilt for existing transcripts" + ); + } + + #[tokio::test] + async fn test_parent_task_id_cascade_delete() { + let pool = fk_test_pool().await; run_migrations(&pool).await.unwrap(); @@ -656,7 +872,7 @@ mod tests { run_migrations(&pool).await.expect("baseline migrate"); const POISON: &[(i64, &str, &str)] = &[( - 9, + 10, "rb-02 atomicity poison", r#" CREATE TABLE poison_marker (id INTEGER PRIMARY KEY); @@ -680,15 +896,14 @@ mod tests { "poison_marker must not exist; got: {marker:?}" ); - // `schema_version` must not include v9 — version insert is part + // `schema_version` must not include v10 — version insert is part // of the same transaction that rolled back. - let max: i64 = - sqlx::query_scalar("SELECT COALESCE(MAX(version), 0) FROM schema_version") - .fetch_one(&pool) - .await - .expect("read schema_version"); + let max: i64 = sqlx::query_scalar("SELECT COALESCE(MAX(version), 0) FROM schema_version") + .fetch_one(&pool) + .await + .expect("read schema_version"); assert_eq!( - max, 8, + max, 9, "schema_version must not advance past the failed migration" ); } diff --git a/crates/transcription/src/lib.rs b/crates/transcription/src/lib.rs index 65357fd..fc26386 100644 --- a/crates/transcription/src/lib.rs +++ b/crates/transcription/src/lib.rs @@ -7,13 +7,13 @@ pub mod transcriber; pub mod whisper_rs_backend; pub use concurrency::run_inference; -pub use local_engine::{load_parakeet, LocalEngine, SpeechModelAdapter, TimedTranscript}; #[cfg(feature = "whisper")] pub use local_engine::load_whisper; +pub use local_engine::{load_parakeet, LocalEngine, SpeechModelAdapter, TimedTranscript}; pub use model_manager::{download, is_downloaded, list_downloaded, model_dir, models_dir}; pub use streaming::{ sample_index_for_seconds, trim_buffer_to_commit_point, CommitDecision, CommitPolicy, LocalAgreement, RmsVadChunker, Token, VadChunk, VadChunker, }; -pub use transcriber::{Transcriber, TranscriberCapabilities}; pub use transcribe_rs::SpeechModel; +pub use transcriber::{Transcriber, TranscriberCapabilities}; diff --git a/crates/transcription/src/model_manager.rs b/crates/transcription/src/model_manager.rs index 0c339c5..f41c003 100644 --- a/crates/transcription/src/model_manager.rs +++ b/crates/transcription/src/model_manager.rs @@ -557,7 +557,10 @@ mod tests { .expect_err("mismatched sha must fail"); let msg = err.to_string(); assert!(msg.contains("SHA256 mismatch"), "unexpected error: {msg}"); - assert!(!dest.exists(), ".part → dest rename must not run on mismatch"); + assert!( + !dest.exists(), + ".part → dest rename must not run on mismatch" + ); let part = dest.with_extension("bin.part"); assert!(!part.exists(), "failed hash must clean up the .part file"); } diff --git a/crates/transcription/src/streaming/commit_policy.rs b/crates/transcription/src/streaming/commit_policy.rs index b573dab..ccd8085 100644 --- a/crates/transcription/src/streaming/commit_policy.rs +++ b/crates/transcription/src/streaming/commit_policy.rs @@ -108,11 +108,7 @@ impl LocalAgreement { // Can't commit anything until we have n passes in hand. if self.history.len() < self.n { - let tentative = self - .history - .back() - .cloned() - .unwrap_or_default(); + let tentative = self.history.back().cloned().unwrap_or_default(); return CommitDecision { newly_committed: Vec::new(), tentative, diff --git a/crates/transcription/src/streaming/rms_vad.rs b/crates/transcription/src/streaming/rms_vad.rs index e4d9e8e..de2cdcd 100644 --- a/crates/transcription/src/streaming/rms_vad.rs +++ b/crates/transcription/src/streaming/rms_vad.rs @@ -121,7 +121,7 @@ impl RmsVadChunker { pending_onset_frames: 0, onset_buffer: Vec::new(), next_sample_index: 0, - active_chunk_start: 0, + active_chunk_start: 0, } } @@ -552,10 +552,7 @@ mod tests { let mut c = RmsVadChunker::new(); assert!(c.flush().is_empty()); let _ = c.push(&constant_signal(16_000, 0.0)); - assert!( - c.flush().is_empty(), - "flushing pure silence emits nothing" - ); + assert!(c.flush().is_empty(), "flushing pure silence emits nothing"); } #[test] diff --git a/crates/transcription/src/whisper_rs_backend.rs b/crates/transcription/src/whisper_rs_backend.rs index b15b30a..4acc38a 100644 --- a/crates/transcription/src/whisper_rs_backend.rs +++ b/crates/transcription/src/whisper_rs_backend.rs @@ -62,10 +62,9 @@ impl Transcriber for WhisperRsBackend { "WhisperRsBackend::transcribe_sync entering" ); - let mut state = self - .ctx - .create_state() - .map_err(|e| KonError::TranscriptionFailed(WhisperBackendError::State(e.to_string()).to_string()))?; + let mut state = self.ctx.create_state().map_err(|e| { + KonError::TranscriptionFailed(WhisperBackendError::State(e.to_string()).to_string()) + })?; let mut params = FullParams::new(SamplingStrategy::Greedy { best_of: 1 }); if let Some(lang) = options.language.as_deref() { @@ -83,9 +82,11 @@ impl Transcriber for WhisperRsBackend { params.set_print_progress(false); params.set_print_realtime(false); - state - .full(params, samples) - .map_err(|e| KonError::TranscriptionFailed(WhisperBackendError::Transcribe(e.to_string()).to_string()))?; + state.full(params, samples).map_err(|e| { + KonError::TranscriptionFailed( + WhisperBackendError::Transcribe(e.to_string()).to_string(), + ) + })?; let n = state.full_n_segments(); @@ -96,7 +97,11 @@ impl Transcriber for WhisperRsBackend { }; let text = seg .to_str() - .map_err(|e| KonError::TranscriptionFailed(WhisperBackendError::Transcribe(e.to_string()).to_string()))? + .map_err(|e| { + KonError::TranscriptionFailed( + WhisperBackendError::Transcribe(e.to_string()).to_string(), + ) + })? .to_string(); // whisper-rs timestamps are centiseconds (10ms units). Convert to seconds (f64). let start = seg.start_timestamp() as f64 * 0.01; diff --git a/docs/issues/README.md b/docs/issues/README.md index 961c571..0cfecbf 100644 --- a/docs/issues/README.md +++ b/docs/issues/README.md @@ -11,43 +11,36 @@ Issues here must land before Kon v0.1 ships. Each is sourced from `docs/code-review-2026-04-22.md`. When `gh` CLI is available, these should be mirrored as real GitHub issues on `jakejars/kon`. -## CRITICAL (2 open, 1 resolved) +## CRITICAL (0 open, 3 resolved) + +No open CRITICAL blockers. + +## MAJOR (1 open, 8 resolved) | # | File | Area | Fix scope | |---|---|---|---| -| RB-01 | [c1-live-session-race.md](c1-live-session-race.md) | `src-tauri/commands/live.rs` | large | -| RB-03 | [c4-transcript-profile-fk.md](c4-transcript-profile-fk.md) | `crates/storage/migrations.rs` + `database.rs` | large | - -## MAJOR (5 open, 4 resolved) - -| # | File | Area | Fix scope | -|---|---|---|---| -| RB-04 | [run-live-session-monolith.md](run-live-session-monolith.md) | `src-tauri/commands/live.rs` | large | -| RB-05 | [poll-inference-channel-fatality.md](poll-inference-channel-fatality.md) | `src-tauri/commands/live.rs` | medium | | RB-08 | [power-assertion-macos-objc2.md](power-assertion-macos-objc2.md) | `src-tauri/commands/power.rs` | medium | -| RB-10 | [llm-prompt-preflight.md](llm-prompt-preflight.md) | `crates/llm/lib.rs` | medium | -| RB-11 | [keystore-thread-safety.md](keystore-thread-safety.md) | `crates/cloud-providers/keystore.rs` | medium | ## Resolved | # | File | Area | Resolution | |---|---|---|---| +| RB-01 | [c1-live-session-race.md](c1-live-session-race.md) | `src-tauri/commands/live.rs` | Added `LiveTranscriptionState.lifecycle: tokio::sync::Mutex<()>` and hold it across the async spans of both `start_live_transcription_session` and `stop_live_transcription_session`. The running-slot check/insert and stop/take/join sequence are now serialized, so concurrent starts can no longer both pass the empty-slot check and a start during stop blocks until the previous worker fully joins. Two async regression tests cover both races. | | RB-02 | [c3-migrations-atomicity.md](c3-migrations-atomicity.md) | `crates/storage/migrations.rs` | Each migration now runs inside a `pool.begin()` / `tx.commit()` transaction alongside its `schema_version` insert. Regression test injects a poisoned v9 migration and asserts neither the partial schema change nor the version row persists. DRY'd `run_migrations_up_to` test helper onto the same code path. | +| RB-03 | [c4-transcript-profile-fk.md](c4-transcript-profile-fk.md) | `crates/storage/migrations.rs` + `database.rs` | Added a transactional v9 rebuild of `transcripts` that enforces `profile_id REFERENCES profiles(id) ON DELETE RESTRICT`, reassigns any orphaned transcript provenance to `DEFAULT_PROFILE_ID`, rebuilds dependent `segments` / FTS state, and preserves valid profile references. `insert_transcript` now rejects unknown profile ids up front, and `delete_profile` returns a clear reassign-first error when transcripts still reference the profile. Regression tests cover migration reconciliation, invalid inserts, and delete rejection. | +| RB-04 | [run-live-session-monolith.md](run-live-session-monolith.md) | `src-tauri/commands/live.rs` | Replaced the 200+ line `run_live_session` loop with an explicit `LiveSessionRuntime` + `LiveLoopState` structure. Capture startup, runtime mic-error draining, audio chunk processing, overflow handling, stop-tail flush, inference dispatch/drain, and WAV finalisation each live in focused helpers, preserving behaviour while making the lifecycle auditable enough for RB-01 follow-up. Existing live tests and the full `kon` lib suite stay green. | +| RB-05 | [poll-inference-channel-fatality.md](poll-inference-channel-fatality.md) | `src-tauri/commands/live.rs` | `poll_inference` now treats result-channel loss as a listener-lifecycle problem rather than a transcription failure. On the first `result_channel.send(...)` error it marks the live result listener as lost, emits a single warning that transcription will continue in the background, and keeps processing later chunks without retrying the dead channel. Regression test simulates a dead result listener and asserts chunk processing continues with only one warning. | | RB-06 | [native-capture-worker-join.md](native-capture-worker-join.md) | `src-tauri/commands/audio.rs` | `NativeCaptureState.stop_tx` replaced by `worker: AsyncMutex>`. `CaptureWorker` bundles the stop sender and the spawned task's `JoinHandle`; `stop_worker(worker)` sends stop then `await`s termination. Both `start_native_capture` (prior-worker stop) and `stop_native_capture` use the helper. Removed the 50ms sleep — the join barrier is exact. Two regression tests cover the lifecycle guarantee and the already-exited case. | | RB-07 | [runtime-capabilities-accelerators.md](runtime-capabilities-accelerators.md) | `src-tauri/commands/models.rs` | Introduced `compose_accelerators(whisper_enabled, loader_available, target)` as a pure helper; `supported_accelerators()` reads `cfg(feature = "whisper")`, `vulkan_loader_available()`, and target OS then delegates. `get_runtime_capabilities` uses it in place of the hard-coded `["cpu", "vulkan"]`. Whisper's `supports_gpu` now follows the feature flag. Five regression tests cover all permutations. | | RB-09 | [decoder-partial-audio-on-error.md](decoder-partial-audio-on-error.md) | `crates/audio/decode.rs` | Packet-loop now propagates all non-EOF `SymphoniaError`s as `AudioDecodeFailed`; per-packet decode errors bubble via `?`. Mock-`MediaSource` regression test confirms mid-stream I/O errors surface instead of returning partial audio. | +| RB-10 | [llm-prompt-preflight.md](llm-prompt-preflight.md) | `crates/llm/lib.rs` | Added an explicit prompt-budget preflight before context creation. If `prompt_tokens + max_tokens + reserve` exceeds the 8192-token cap, `generate` now returns a typed `EngineError::PromptTooLong { ... }` instead of failing late inside inference. Regression tests cover both the over-budget and exact-budget boundaries. | +| RB-11 | [keystore-thread-safety.md](keystore-thread-safety.md) | `crates/cloud-providers/keystore.rs` | Replaced the `std::env::set_var` stub with a process-global `OnceLock>>` keystore, keeping the API safe from any thread. Retrieval still falls back to read-only `KON_API_KEY_*` env vars for externally supplied secrets. Two regression tests cover store/retrieve and provider isolation. | | RB-12 | [hotkey-linux-device-filter.md](hotkey-linux-device-filter.md) | `crates/hotkey/linux.rs` | Extracted `device_supports_combo` helper; `try_attach_device` now reads the configured `HotkeyCombo` from the watch channel and checks support for that trigger key. Four regression tests land in `linux::tests`. | -## Dependencies +## Remaining blocker -``` -RB-01 (live session race) - └── blocked by RB-04 (run_live_session monolith refactor) - -RB-03 (transcript-profile FK) - └── coupled with RB-02 (migrations atomicity) - — a v9 migration adding the FK constraint must be transactional -``` +`RB-08` remains open pending manual runtime verification on a real macOS +machine (`pmset -g assertions`, background live-session sanity check). ## How to convert to GitHub issues diff --git a/docs/issues/c1-live-session-race.md b/docs/issues/c1-live-session-race.md index 8ebf6cd..a8a48ba 100644 --- a/docs/issues/c1-live-session-race.md +++ b/docs/issues/c1-live-session-race.md @@ -4,6 +4,31 @@ **Path:** `src-tauri/src/commands/live.rs:193-338` **Source:** [2026-04-22 code review](../code-review-2026-04-22.md#c1--racy-single-session-guard-in-livers) **Labels:** release-blocker, critical, concurrency +**Status:** RESOLVED (2026-04-22) + +## Resolution + +`LiveTranscriptionState` now includes a dedicated +`tokio::sync::Mutex<()>` lifecycle gate. Both +`start_live_transcription_session` and +`stop_live_transcription_session` acquire that async mutex before +touching `running`, and they keep it held across the awaited setup / +join work that previously exposed the race windows. + +That changes the two failing interleavings from the review: + +- Two overlapping starts no longer race through the empty-slot check. + The second call waits for the first to finish setup, then observes + `running.is_some()` and returns the existing + `"A live transcription session is already running"` error. +- A start launched during stop can no longer sneak in after + `running.take()` but before the previous worker has fully joined. + It blocks on the lifecycle mutex until the join completes. + +Regression tests in `commands::live::tests`: + +- `concurrent_starts_allow_only_one_session_to_claim_the_slot` +- `start_waits_for_stop_to_finish_joining_before_reusing_slot` ## Problem @@ -26,4 +51,4 @@ Large. Will likely require the `run_live_session` monolith refactor (RB-04) to l ## Dependencies -- **Blocked by:** RB-04 (`run_live_session` monolith refactor) +- Landed after RB-04 (`run_live_session` refactor) made the worker lifecycle explicit enough to guard safely. diff --git a/docs/issues/c4-transcript-profile-fk.md b/docs/issues/c4-transcript-profile-fk.md index 96d7d65..f2dc8f4 100644 --- a/docs/issues/c4-transcript-profile-fk.md +++ b/docs/issues/c4-transcript-profile-fk.md @@ -4,6 +4,33 @@ **Path:** `crates/storage/src/migrations.rs:208-216`, `crates/storage/src/database.rs:61-89`, `:697-708` **Source:** [2026-04-22 code review](../code-review-2026-04-22.md#c4--transcript-provenance-can-reference-deleted-profiles) **Labels:** release-blocker, critical, data-integrity, storage +**Status:** RESOLVED (2026-04-22) + +## Resolution + +Chose the strict provenance path: + +- Migration v9 rebuilds `transcripts` with + `profile_id REFERENCES profiles(id) ON DELETE RESTRICT`. +- Existing orphaned transcript `profile_id` values are reconciled onto + `DEFAULT_PROFILE_ID` during the copy into the rebuilt table. +- Because SQLite table renames rewrite dependent references, the + migration also rebuilds `segments`, recreates the transcript FTS + virtual table/triggers, and repopulates FTS from the rebuilt + transcript rows inside the same transaction. + +Application-layer behaviour now matches the schema: + +- `insert_transcript` rejects unknown `profile_id` values with a clear + storage error before attempting the insert. +- `delete_profile` returns a human-readable reassign-first error when + transcripts still reference that profile. + +Regression tests: + +- `migration_v9_reconciles_orphaned_transcript_profiles_and_adds_fk` +- `insert_transcript_rejects_unknown_profile_id` +- `delete_profile_rejects_when_transcripts_reference_it` ## Problem diff --git a/docs/issues/keystore-thread-safety.md b/docs/issues/keystore-thread-safety.md index 71be19e..f73c223 100644 --- a/docs/issues/keystore-thread-safety.md +++ b/docs/issues/keystore-thread-safety.md @@ -4,6 +4,28 @@ **Path:** `crates/cloud-providers/src/keystore.rs:6-18` **Source:** [2026-04-22 code review](../code-review-2026-04-22.md) **Labels:** release-blocker, major, unsafe-api, cloud +**Status:** RESOLVED (2026-04-22) + +## Resolution + +Chose acceptance option 2. The environment-mutation stub is gone; +`store_api_key` now writes into a process-global +`OnceLock>>`, so the safe signature matches +the actual safety properties. + +Additional details: + +- Stored keys now live in-memory only for the life of the process. +- `retrieve_api_key` checks the in-memory keystore first, then falls + back to read-only `KON_API_KEY_` environment variables so + externally injected secrets still work. +- Module docs now describe the real tradeoff clearly: safe from any + thread, but non-persistent until a proper OS keychain backend lands. + +Regression tests: + +- `stored_key_is_retrievable_without_env_mutation` +- `providers_do_not_overlap` ## Problem diff --git a/docs/issues/llm-prompt-preflight.md b/docs/issues/llm-prompt-preflight.md index e1a4e09..0a64bc3 100644 --- a/docs/issues/llm-prompt-preflight.md +++ b/docs/issues/llm-prompt-preflight.md @@ -4,6 +4,25 @@ **Path:** `crates/llm/src/lib.rs:143-166`, `:317-321` **Source:** [2026-04-22 code review](../code-review-2026-04-22.md) **Labels:** release-blocker, major, llm +**Status:** RESOLVED (2026-04-22) + +## Resolution + +`LlmEngine::generate` still tokenises the whole prompt up front, but it +now runs a dedicated prompt-budget preflight before creating the llama +context. The chosen behaviour is an early typed failure rather than +silent truncation: + +- If `prompt_tokens + max_tokens + 64 reserve tokens` exceeds the + 8192-token cap, generation returns + `EngineError::PromptTooLong { prompt_tokens, max_tokens, available_prompt_tokens, context_window }`. +- Prompts that fit exactly within the available budget still proceed and + allocate an 8192-token context as before. + +Regression tests: + +- `prompt_preflight_rejects_oversized_prompt_tokens` +- `prompt_preflight_keeps_prompts_within_budget` ## Problem diff --git a/docs/issues/poll-inference-channel-fatality.md b/docs/issues/poll-inference-channel-fatality.md index 921d5dd..36d6c1f 100644 --- a/docs/issues/poll-inference-channel-fatality.md +++ b/docs/issues/poll-inference-channel-fatality.md @@ -4,6 +4,31 @@ **Path:** `src-tauri/src/commands/live.rs:721-813` **Source:** [2026-04-22 code review](../code-review-2026-04-22.md) **Labels:** release-blocker, major, ipc-lifecycle +**Status:** RESOLVED (2026-04-22) + +## Resolution + +`poll_inference` no longer propagates `result_channel.send(...)` with `?`. +Instead, live-result delivery is routed through a small helper that +tracks whether the frontend listener has already been lost: + +- First send failure: mark the result listener as unavailable, log a + warning, and best-effort send a `LiveStatusMessage::Warning` + explaining that transcription will continue in the background until + the user stops the session. +- Subsequent chunks: skip re-sending to the dead result channel and + keep the worker running. + +Crucially, this path is now separate from actual transcription failure: +inference errors still emit `LiveStatusMessage::Error` and stop the +session, while listener-loss just stops live preview delivery. + +Regression test: + +- `result_listener_loss_is_warned_once_and_not_treated_as_inference_failure` + simulates a dead result channel, confirms the first processed chunk + downgrades to a warning, and confirms a second chunk still processes + successfully without a second warning. ## Problem diff --git a/docs/issues/power-assertion-macos-objc2.md b/docs/issues/power-assertion-macos-objc2.md index d53ba9a..acec03a 100644 --- a/docs/issues/power-assertion-macos-objc2.md +++ b/docs/issues/power-assertion-macos-objc2.md @@ -5,6 +5,18 @@ **Source:** [2026-04-22 code review](../code-review-2026-04-22.md), originally deferred during A.1 #9 **Labels:** release-blocker, major, macos, platform +**Current state (2026-04-23):** `objc2`/`objc2-foundation` have been +added behind `cfg(target_os = "macos")`, and the `NSProcessInfo` +bridge now calls `beginActivityWithOptions:reason:` / `endActivity:` +with the retained activity handle. Isolated `cargo check` validation +passes for both `x86_64-apple-darwin` and `aarch64-apple-darwin`. +Remaining acceptance gap: manual runtime verification on a real macOS +machine (`pmset -g assertions`, background live session). Diagnostic +reports now also include a `## Power assertions` section that lists any +currently active Kon assertion guards (`reason`, `backend`, `acquired`) +at report time, which gives the tester an in-app breadcrumb alongside +`pmset`. + ## Problem `begin_activity` always returns `Err` on macOS, so `PowerAssertion::begin` converts to `None` and the guard never acquires an `NSProcessInfo beginActivityWithOptions:reason:` assertion. Live recording and LLM cleanup therefore run without App Nap protection on the one platform where it matters. @@ -18,6 +30,21 @@ The stub was deliberate (A.1 #9 acceptance concession — untestable on Linux wi - `end_activity` calls `endActivity:` on the retained handle. - Manual-test on a real macOS box: 10-minute background live session completes without throttling; `pmset -g assertions` shows Kon's activity during capture. +## Manual verification checklist + +1. Launch Kon on a real macOS machine and start a live transcription session. +2. While capture is running, background the app for at least several minutes. +3. In Terminal, run `pmset -g assertions` and confirm Kon appears with a + user-initiated / no-idle-style assertion while the session is active. +4. While the session is still running, generate a Kon diagnostic report + and confirm the `## Power assertions` section lists an active entry + such as `reason=kon live dictation session`, `backend=macos`, + `acquired=true`. +5. Stop the session and rerun `pmset -g assertions` or regenerate the + diagnostic report to confirm the assertion disappears. +6. Repeat once for the LLM cleanup path if desired + (`reason=kon LLM cleanup`). + ## Fix scope Medium. Dep addition + FFI glue + manual verification. Can be done from Linux with `cargo check --target=aarch64-apple-darwin` for compile validation, but runtime behaviour needs a macOS machine. diff --git a/docs/issues/run-live-session-monolith.md b/docs/issues/run-live-session-monolith.md index 3a43bd4..3cfe97e 100644 --- a/docs/issues/run-live-session-monolith.md +++ b/docs/issues/run-live-session-monolith.md @@ -4,6 +4,33 @@ **Path:** `src-tauri/src/commands/live.rs:349-579` **Source:** [2026-04-22 code review](../code-review-2026-04-22.md) **Labels:** release-blocker, major, refactor, concurrency +**Status:** RESOLVED (2026-04-22) + +## Resolution + +`run_live_session` was split around two explicit state holders: + +- `ActiveCapture` owns the cpal stream handle, audio receiver, and + optional runtime-error channel. +- `LiveLoopState` owns the mutable per-session loop state: resampler, + capture buffer, WAV writer, buffer offsets, dropped-audio accounting, + in-flight inference task, and duplicate-history buffer. + +The top-level worker is now `LiveSessionRuntime`, with focused methods +for: + +- polling in-flight inference +- draining microphone runtime warnings +- receiving + resampling an audio chunk +- dropping pending-buffer overflow +- flushing the resampler tail when stop is requested +- dispatching inference when enough audio is buffered +- draining the last in-flight task +- finalising the progressive WAV writer + +This keeps behaviour intact but removes the "everything in one mutable +loop" shape that made concurrency review hard. The refactor also made +RB-01 straightforward enough to land immediately afterward. ## Problem diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d2220e6..56f5b5f 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -74,3 +74,7 @@ webkit2gtk = "2.0" # transitively depends on (GTK 3). gtk = "0.18" gdk = "0.18" + +[target.'cfg(target_os = "macos")'.dependencies] +objc2 = "0.6.4" +objc2-foundation = { version = "0.3.2", default-features = false, features = ["std", "NSString", "NSProcessInfo"] } diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 5b7c7ea..0ed0955 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -33,8 +33,8 @@ fn assert_localhost_llm_csp() { let raw = std::fs::read_to_string(conf_path) .expect("build.rs: failed to read tauri.conf.json for CSP regression guard"); - let conf: serde_json::Value = serde_json::from_str(&raw) - .expect("build.rs: tauri.conf.json is not valid JSON"); + let conf: serde_json::Value = + serde_json::from_str(&raw).expect("build.rs: tauri.conf.json is not valid JSON"); let csp = conf .pointer("/app/security/csp") diff --git a/src-tauri/gen/schemas/acl-manifests.json b/src-tauri/gen/schemas/acl-manifests.json index c7e170f..e559af5 100644 --- a/src-tauri/gen/schemas/acl-manifests.json +++ b/src-tauri/gen/schemas/acl-manifests.json @@ -1 +1 @@ -{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)","commands":{"allow":["message"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)","commands":{"allow":[],"deny":["message"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"global-shortcut":{"default_permission":{"identifier":"default","description":"No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n","permissions":[]},"permissions":{"allow-is-registered":{"identifier":"allow-is-registered","description":"Enables the is_registered command without any pre-configured scope.","commands":{"allow":["is_registered"],"deny":[]}},"allow-register":{"identifier":"allow-register","description":"Enables the register command without any pre-configured scope.","commands":{"allow":["register"],"deny":[]}},"allow-register-all":{"identifier":"allow-register-all","description":"Enables the register_all command without any pre-configured scope.","commands":{"allow":["register_all"],"deny":[]}},"allow-unregister":{"identifier":"allow-unregister","description":"Enables the unregister command without any pre-configured scope.","commands":{"allow":["unregister"],"deny":[]}},"allow-unregister-all":{"identifier":"allow-unregister-all","description":"Enables the unregister_all command without any pre-configured scope.","commands":{"allow":["unregister_all"],"deny":[]}},"deny-is-registered":{"identifier":"deny-is-registered","description":"Denies the is_registered command without any pre-configured scope.","commands":{"allow":[],"deny":["is_registered"]}},"deny-register":{"identifier":"deny-register","description":"Denies the register command without any pre-configured scope.","commands":{"allow":[],"deny":["register"]}},"deny-register-all":{"identifier":"deny-register-all","description":"Denies the register_all command without any pre-configured scope.","commands":{"allow":[],"deny":["register_all"]}},"deny-unregister":{"identifier":"deny-unregister","description":"Denies the unregister command without any pre-configured scope.","commands":{"allow":[],"deny":["unregister"]}},"deny-unregister-all":{"identifier":"deny-unregister-all","description":"Denies the unregister_all command without any pre-configured scope.","commands":{"allow":[],"deny":["unregister_all"]}}},"permission_sets":{},"global_scope_schema":null},"opener":{"default_permission":{"identifier":"default","description":"This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer","permissions":["allow-open-url","allow-reveal-item-in-dir","allow-default-urls"]},"permissions":{"allow-default-urls":{"identifier":"allow-default-urls","description":"This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"url":"mailto:*"},{"url":"tel:*"},{"url":"http://*"},{"url":"https://*"}]}},"allow-open-path":{"identifier":"allow-open-path","description":"Enables the open_path command without any pre-configured scope.","commands":{"allow":["open_path"],"deny":[]}},"allow-open-url":{"identifier":"allow-open-url","description":"Enables the open_url command without any pre-configured scope.","commands":{"allow":["open_url"],"deny":[]}},"allow-reveal-item-in-dir":{"identifier":"allow-reveal-item-in-dir","description":"Enables the reveal_item_in_dir command without any pre-configured scope.","commands":{"allow":["reveal_item_in_dir"],"deny":[]}},"deny-open-path":{"identifier":"deny-open-path","description":"Denies the open_path command without any pre-configured scope.","commands":{"allow":[],"deny":["open_path"]}},"deny-open-url":{"identifier":"deny-open-url","description":"Denies the open_url command without any pre-configured scope.","commands":{"allow":[],"deny":["open_url"]}},"deny-reveal-item-in-dir":{"identifier":"deny-reveal-item-in-dir","description":"Denies the reveal_item_in_dir command without any pre-configured scope.","commands":{"allow":[],"deny":["reveal_item_in_dir"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"properties":{"app":{"allOf":[{"$ref":"#/definitions/Application"}],"description":"An application to open this url with, for example: firefox."},"url":{"description":"A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"","type":"string"}},"required":["url"],"type":"object"},{"properties":{"app":{"allOf":[{"$ref":"#/definitions/Application"}],"description":"An application to open this path with, for example: xdg-open."},"path":{"description":"A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"}},"required":["path"],"type":"object"}],"definitions":{"Application":{"anyOf":[{"description":"Open in default application.","type":"null"},{"description":"If true, allow open with any application.","type":"boolean"},{"description":"Allow specific application to open with.","type":"string"}],"description":"Opener scope application."}},"description":"Opener scope entry.","title":"OpenerScopeEntry"}},"updater":{"default_permission":{"identifier":"default","description":"This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n","permissions":["allow-check","allow-download","allow-install","allow-download-and-install"]},"permissions":{"allow-check":{"identifier":"allow-check","description":"Enables the check command without any pre-configured scope.","commands":{"allow":["check"],"deny":[]}},"allow-download":{"identifier":"allow-download","description":"Enables the download command without any pre-configured scope.","commands":{"allow":["download"],"deny":[]}},"allow-download-and-install":{"identifier":"allow-download-and-install","description":"Enables the download_and_install command without any pre-configured scope.","commands":{"allow":["download_and_install"],"deny":[]}},"allow-install":{"identifier":"allow-install","description":"Enables the install command without any pre-configured scope.","commands":{"allow":["install"],"deny":[]}},"deny-check":{"identifier":"deny-check","description":"Denies the check command without any pre-configured scope.","commands":{"allow":[],"deny":["check"]}},"deny-download":{"identifier":"deny-download","description":"Denies the download command without any pre-configured scope.","commands":{"allow":[],"deny":["download"]}},"deny-download-and-install":{"identifier":"deny-download-and-install","description":"Denies the download_and_install command without any pre-configured scope.","commands":{"allow":[],"deny":["download_and_install"]}},"deny-install":{"identifier":"deny-install","description":"Denies the install command without any pre-configured scope.","commands":{"allow":[],"deny":["install"]}}},"permission_sets":{},"global_scope_schema":null},"window-state":{"default_permission":{"identifier":"default","description":"This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n","permissions":["allow-filename","allow-restore-state","allow-save-window-state"]},"permissions":{"allow-filename":{"identifier":"allow-filename","description":"Enables the filename command without any pre-configured scope.","commands":{"allow":["filename"],"deny":[]}},"allow-restore-state":{"identifier":"allow-restore-state","description":"Enables the restore_state command without any pre-configured scope.","commands":{"allow":["restore_state"],"deny":[]}},"allow-save-window-state":{"identifier":"allow-save-window-state","description":"Enables the save_window_state command without any pre-configured scope.","commands":{"allow":["save_window_state"],"deny":[]}},"deny-filename":{"identifier":"deny-filename","description":"Denies the filename command without any pre-configured scope.","commands":{"allow":[],"deny":["filename"]}},"deny-restore-state":{"identifier":"deny-restore-state","description":"Denies the restore_state command without any pre-configured scope.","commands":{"allow":[],"deny":["restore_state"]}},"deny-save-window-state":{"identifier":"deny-save-window-state","description":"Denies the save_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["save_window_state"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file +{"core":{"default_permission":{"identifier":"default","description":"Default core plugins set.","permissions":["core:path:default","core:event:default","core:window:default","core:webview:default","core:app:default","core:image:default","core:resources:default","core:menu:default","core:tray:default"]},"permissions":{},"permission_sets":{},"global_scope_schema":null},"core:app":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-version","allow-name","allow-tauri-version","allow-identifier","allow-bundle-type","allow-register-listener","allow-remove-listener"]},"permissions":{"allow-app-hide":{"identifier":"allow-app-hide","description":"Enables the app_hide command without any pre-configured scope.","commands":{"allow":["app_hide"],"deny":[]}},"allow-app-show":{"identifier":"allow-app-show","description":"Enables the app_show command without any pre-configured scope.","commands":{"allow":["app_show"],"deny":[]}},"allow-bundle-type":{"identifier":"allow-bundle-type","description":"Enables the bundle_type command without any pre-configured scope.","commands":{"allow":["bundle_type"],"deny":[]}},"allow-default-window-icon":{"identifier":"allow-default-window-icon","description":"Enables the default_window_icon command without any pre-configured scope.","commands":{"allow":["default_window_icon"],"deny":[]}},"allow-fetch-data-store-identifiers":{"identifier":"allow-fetch-data-store-identifiers","description":"Enables the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":["fetch_data_store_identifiers"],"deny":[]}},"allow-identifier":{"identifier":"allow-identifier","description":"Enables the identifier command without any pre-configured scope.","commands":{"allow":["identifier"],"deny":[]}},"allow-name":{"identifier":"allow-name","description":"Enables the name command without any pre-configured scope.","commands":{"allow":["name"],"deny":[]}},"allow-register-listener":{"identifier":"allow-register-listener","description":"Enables the register_listener command without any pre-configured scope.","commands":{"allow":["register_listener"],"deny":[]}},"allow-remove-data-store":{"identifier":"allow-remove-data-store","description":"Enables the remove_data_store command without any pre-configured scope.","commands":{"allow":["remove_data_store"],"deny":[]}},"allow-remove-listener":{"identifier":"allow-remove-listener","description":"Enables the remove_listener command without any pre-configured scope.","commands":{"allow":["remove_listener"],"deny":[]}},"allow-set-app-theme":{"identifier":"allow-set-app-theme","description":"Enables the set_app_theme command without any pre-configured scope.","commands":{"allow":["set_app_theme"],"deny":[]}},"allow-set-dock-visibility":{"identifier":"allow-set-dock-visibility","description":"Enables the set_dock_visibility command without any pre-configured scope.","commands":{"allow":["set_dock_visibility"],"deny":[]}},"allow-tauri-version":{"identifier":"allow-tauri-version","description":"Enables the tauri_version command without any pre-configured scope.","commands":{"allow":["tauri_version"],"deny":[]}},"allow-version":{"identifier":"allow-version","description":"Enables the version command without any pre-configured scope.","commands":{"allow":["version"],"deny":[]}},"deny-app-hide":{"identifier":"deny-app-hide","description":"Denies the app_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["app_hide"]}},"deny-app-show":{"identifier":"deny-app-show","description":"Denies the app_show command without any pre-configured scope.","commands":{"allow":[],"deny":["app_show"]}},"deny-bundle-type":{"identifier":"deny-bundle-type","description":"Denies the bundle_type command without any pre-configured scope.","commands":{"allow":[],"deny":["bundle_type"]}},"deny-default-window-icon":{"identifier":"deny-default-window-icon","description":"Denies the default_window_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["default_window_icon"]}},"deny-fetch-data-store-identifiers":{"identifier":"deny-fetch-data-store-identifiers","description":"Denies the fetch_data_store_identifiers command without any pre-configured scope.","commands":{"allow":[],"deny":["fetch_data_store_identifiers"]}},"deny-identifier":{"identifier":"deny-identifier","description":"Denies the identifier command without any pre-configured scope.","commands":{"allow":[],"deny":["identifier"]}},"deny-name":{"identifier":"deny-name","description":"Denies the name command without any pre-configured scope.","commands":{"allow":[],"deny":["name"]}},"deny-register-listener":{"identifier":"deny-register-listener","description":"Denies the register_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["register_listener"]}},"deny-remove-data-store":{"identifier":"deny-remove-data-store","description":"Denies the remove_data_store command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_data_store"]}},"deny-remove-listener":{"identifier":"deny-remove-listener","description":"Denies the remove_listener command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_listener"]}},"deny-set-app-theme":{"identifier":"deny-set-app-theme","description":"Denies the set_app_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_app_theme"]}},"deny-set-dock-visibility":{"identifier":"deny-set-dock-visibility","description":"Denies the set_dock_visibility command without any pre-configured scope.","commands":{"allow":[],"deny":["set_dock_visibility"]}},"deny-tauri-version":{"identifier":"deny-tauri-version","description":"Denies the tauri_version command without any pre-configured scope.","commands":{"allow":[],"deny":["tauri_version"]}},"deny-version":{"identifier":"deny-version","description":"Denies the version command without any pre-configured scope.","commands":{"allow":[],"deny":["version"]}}},"permission_sets":{},"global_scope_schema":null},"core:event":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-listen","allow-unlisten","allow-emit","allow-emit-to"]},"permissions":{"allow-emit":{"identifier":"allow-emit","description":"Enables the emit command without any pre-configured scope.","commands":{"allow":["emit"],"deny":[]}},"allow-emit-to":{"identifier":"allow-emit-to","description":"Enables the emit_to command without any pre-configured scope.","commands":{"allow":["emit_to"],"deny":[]}},"allow-listen":{"identifier":"allow-listen","description":"Enables the listen command without any pre-configured scope.","commands":{"allow":["listen"],"deny":[]}},"allow-unlisten":{"identifier":"allow-unlisten","description":"Enables the unlisten command without any pre-configured scope.","commands":{"allow":["unlisten"],"deny":[]}},"deny-emit":{"identifier":"deny-emit","description":"Denies the emit command without any pre-configured scope.","commands":{"allow":[],"deny":["emit"]}},"deny-emit-to":{"identifier":"deny-emit-to","description":"Denies the emit_to command without any pre-configured scope.","commands":{"allow":[],"deny":["emit_to"]}},"deny-listen":{"identifier":"deny-listen","description":"Denies the listen command without any pre-configured scope.","commands":{"allow":[],"deny":["listen"]}},"deny-unlisten":{"identifier":"deny-unlisten","description":"Denies the unlisten command without any pre-configured scope.","commands":{"allow":[],"deny":["unlisten"]}}},"permission_sets":{},"global_scope_schema":null},"core:image":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-from-bytes","allow-from-path","allow-rgba","allow-size"]},"permissions":{"allow-from-bytes":{"identifier":"allow-from-bytes","description":"Enables the from_bytes command without any pre-configured scope.","commands":{"allow":["from_bytes"],"deny":[]}},"allow-from-path":{"identifier":"allow-from-path","description":"Enables the from_path command without any pre-configured scope.","commands":{"allow":["from_path"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-rgba":{"identifier":"allow-rgba","description":"Enables the rgba command without any pre-configured scope.","commands":{"allow":["rgba"],"deny":[]}},"allow-size":{"identifier":"allow-size","description":"Enables the size command without any pre-configured scope.","commands":{"allow":["size"],"deny":[]}},"deny-from-bytes":{"identifier":"deny-from-bytes","description":"Denies the from_bytes command without any pre-configured scope.","commands":{"allow":[],"deny":["from_bytes"]}},"deny-from-path":{"identifier":"deny-from-path","description":"Denies the from_path command without any pre-configured scope.","commands":{"allow":[],"deny":["from_path"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-rgba":{"identifier":"deny-rgba","description":"Denies the rgba command without any pre-configured scope.","commands":{"allow":[],"deny":["rgba"]}},"deny-size":{"identifier":"deny-size","description":"Denies the size command without any pre-configured scope.","commands":{"allow":[],"deny":["size"]}}},"permission_sets":{},"global_scope_schema":null},"core:menu":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-append","allow-prepend","allow-insert","allow-remove","allow-remove-at","allow-items","allow-get","allow-popup","allow-create-default","allow-set-as-app-menu","allow-set-as-window-menu","allow-text","allow-set-text","allow-is-enabled","allow-set-enabled","allow-set-accelerator","allow-set-as-windows-menu-for-nsapp","allow-set-as-help-menu-for-nsapp","allow-is-checked","allow-set-checked","allow-set-icon"]},"permissions":{"allow-append":{"identifier":"allow-append","description":"Enables the append command without any pre-configured scope.","commands":{"allow":["append"],"deny":[]}},"allow-create-default":{"identifier":"allow-create-default","description":"Enables the create_default command without any pre-configured scope.","commands":{"allow":["create_default"],"deny":[]}},"allow-get":{"identifier":"allow-get","description":"Enables the get command without any pre-configured scope.","commands":{"allow":["get"],"deny":[]}},"allow-insert":{"identifier":"allow-insert","description":"Enables the insert command without any pre-configured scope.","commands":{"allow":["insert"],"deny":[]}},"allow-is-checked":{"identifier":"allow-is-checked","description":"Enables the is_checked command without any pre-configured scope.","commands":{"allow":["is_checked"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-items":{"identifier":"allow-items","description":"Enables the items command without any pre-configured scope.","commands":{"allow":["items"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-popup":{"identifier":"allow-popup","description":"Enables the popup command without any pre-configured scope.","commands":{"allow":["popup"],"deny":[]}},"allow-prepend":{"identifier":"allow-prepend","description":"Enables the prepend command without any pre-configured scope.","commands":{"allow":["prepend"],"deny":[]}},"allow-remove":{"identifier":"allow-remove","description":"Enables the remove command without any pre-configured scope.","commands":{"allow":["remove"],"deny":[]}},"allow-remove-at":{"identifier":"allow-remove-at","description":"Enables the remove_at command without any pre-configured scope.","commands":{"allow":["remove_at"],"deny":[]}},"allow-set-accelerator":{"identifier":"allow-set-accelerator","description":"Enables the set_accelerator command without any pre-configured scope.","commands":{"allow":["set_accelerator"],"deny":[]}},"allow-set-as-app-menu":{"identifier":"allow-set-as-app-menu","description":"Enables the set_as_app_menu command without any pre-configured scope.","commands":{"allow":["set_as_app_menu"],"deny":[]}},"allow-set-as-help-menu-for-nsapp":{"identifier":"allow-set-as-help-menu-for-nsapp","description":"Enables the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_help_menu_for_nsapp"],"deny":[]}},"allow-set-as-window-menu":{"identifier":"allow-set-as-window-menu","description":"Enables the set_as_window_menu command without any pre-configured scope.","commands":{"allow":["set_as_window_menu"],"deny":[]}},"allow-set-as-windows-menu-for-nsapp":{"identifier":"allow-set-as-windows-menu-for-nsapp","description":"Enables the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":["set_as_windows_menu_for_nsapp"],"deny":[]}},"allow-set-checked":{"identifier":"allow-set-checked","description":"Enables the set_checked command without any pre-configured scope.","commands":{"allow":["set_checked"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-text":{"identifier":"allow-set-text","description":"Enables the set_text command without any pre-configured scope.","commands":{"allow":["set_text"],"deny":[]}},"allow-text":{"identifier":"allow-text","description":"Enables the text command without any pre-configured scope.","commands":{"allow":["text"],"deny":[]}},"deny-append":{"identifier":"deny-append","description":"Denies the append command without any pre-configured scope.","commands":{"allow":[],"deny":["append"]}},"deny-create-default":{"identifier":"deny-create-default","description":"Denies the create_default command without any pre-configured scope.","commands":{"allow":[],"deny":["create_default"]}},"deny-get":{"identifier":"deny-get","description":"Denies the get command without any pre-configured scope.","commands":{"allow":[],"deny":["get"]}},"deny-insert":{"identifier":"deny-insert","description":"Denies the insert command without any pre-configured scope.","commands":{"allow":[],"deny":["insert"]}},"deny-is-checked":{"identifier":"deny-is-checked","description":"Denies the is_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["is_checked"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-items":{"identifier":"deny-items","description":"Denies the items command without any pre-configured scope.","commands":{"allow":[],"deny":["items"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-popup":{"identifier":"deny-popup","description":"Denies the popup command without any pre-configured scope.","commands":{"allow":[],"deny":["popup"]}},"deny-prepend":{"identifier":"deny-prepend","description":"Denies the prepend command without any pre-configured scope.","commands":{"allow":[],"deny":["prepend"]}},"deny-remove":{"identifier":"deny-remove","description":"Denies the remove command without any pre-configured scope.","commands":{"allow":[],"deny":["remove"]}},"deny-remove-at":{"identifier":"deny-remove-at","description":"Denies the remove_at command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_at"]}},"deny-set-accelerator":{"identifier":"deny-set-accelerator","description":"Denies the set_accelerator command without any pre-configured scope.","commands":{"allow":[],"deny":["set_accelerator"]}},"deny-set-as-app-menu":{"identifier":"deny-set-as-app-menu","description":"Denies the set_as_app_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_app_menu"]}},"deny-set-as-help-menu-for-nsapp":{"identifier":"deny-set-as-help-menu-for-nsapp","description":"Denies the set_as_help_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_help_menu_for_nsapp"]}},"deny-set-as-window-menu":{"identifier":"deny-set-as-window-menu","description":"Denies the set_as_window_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_window_menu"]}},"deny-set-as-windows-menu-for-nsapp":{"identifier":"deny-set-as-windows-menu-for-nsapp","description":"Denies the set_as_windows_menu_for_nsapp command without any pre-configured scope.","commands":{"allow":[],"deny":["set_as_windows_menu_for_nsapp"]}},"deny-set-checked":{"identifier":"deny-set-checked","description":"Denies the set_checked command without any pre-configured scope.","commands":{"allow":[],"deny":["set_checked"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-text":{"identifier":"deny-set-text","description":"Denies the set_text command without any pre-configured scope.","commands":{"allow":[],"deny":["set_text"]}},"deny-text":{"identifier":"deny-text","description":"Denies the text command without any pre-configured scope.","commands":{"allow":[],"deny":["text"]}}},"permission_sets":{},"global_scope_schema":null},"core:path":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-resolve-directory","allow-resolve","allow-normalize","allow-join","allow-dirname","allow-extname","allow-basename","allow-is-absolute"]},"permissions":{"allow-basename":{"identifier":"allow-basename","description":"Enables the basename command without any pre-configured scope.","commands":{"allow":["basename"],"deny":[]}},"allow-dirname":{"identifier":"allow-dirname","description":"Enables the dirname command without any pre-configured scope.","commands":{"allow":["dirname"],"deny":[]}},"allow-extname":{"identifier":"allow-extname","description":"Enables the extname command without any pre-configured scope.","commands":{"allow":["extname"],"deny":[]}},"allow-is-absolute":{"identifier":"allow-is-absolute","description":"Enables the is_absolute command without any pre-configured scope.","commands":{"allow":["is_absolute"],"deny":[]}},"allow-join":{"identifier":"allow-join","description":"Enables the join command without any pre-configured scope.","commands":{"allow":["join"],"deny":[]}},"allow-normalize":{"identifier":"allow-normalize","description":"Enables the normalize command without any pre-configured scope.","commands":{"allow":["normalize"],"deny":[]}},"allow-resolve":{"identifier":"allow-resolve","description":"Enables the resolve command without any pre-configured scope.","commands":{"allow":["resolve"],"deny":[]}},"allow-resolve-directory":{"identifier":"allow-resolve-directory","description":"Enables the resolve_directory command without any pre-configured scope.","commands":{"allow":["resolve_directory"],"deny":[]}},"deny-basename":{"identifier":"deny-basename","description":"Denies the basename command without any pre-configured scope.","commands":{"allow":[],"deny":["basename"]}},"deny-dirname":{"identifier":"deny-dirname","description":"Denies the dirname command without any pre-configured scope.","commands":{"allow":[],"deny":["dirname"]}},"deny-extname":{"identifier":"deny-extname","description":"Denies the extname command without any pre-configured scope.","commands":{"allow":[],"deny":["extname"]}},"deny-is-absolute":{"identifier":"deny-is-absolute","description":"Denies the is_absolute command without any pre-configured scope.","commands":{"allow":[],"deny":["is_absolute"]}},"deny-join":{"identifier":"deny-join","description":"Denies the join command without any pre-configured scope.","commands":{"allow":[],"deny":["join"]}},"deny-normalize":{"identifier":"deny-normalize","description":"Denies the normalize command without any pre-configured scope.","commands":{"allow":[],"deny":["normalize"]}},"deny-resolve":{"identifier":"deny-resolve","description":"Denies the resolve command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve"]}},"deny-resolve-directory":{"identifier":"deny-resolve-directory","description":"Denies the resolve_directory command without any pre-configured scope.","commands":{"allow":[],"deny":["resolve_directory"]}}},"permission_sets":{},"global_scope_schema":null},"core:resources":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-close"]},"permissions":{"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}}},"permission_sets":{},"global_scope_schema":null},"core:tray":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin, which enables all commands.","permissions":["allow-new","allow-get-by-id","allow-remove-by-id","allow-set-icon","allow-set-menu","allow-set-tooltip","allow-set-title","allow-set-visible","allow-set-temp-dir-path","allow-set-icon-as-template","allow-set-show-menu-on-left-click"]},"permissions":{"allow-get-by-id":{"identifier":"allow-get-by-id","description":"Enables the get_by_id command without any pre-configured scope.","commands":{"allow":["get_by_id"],"deny":[]}},"allow-new":{"identifier":"allow-new","description":"Enables the new command without any pre-configured scope.","commands":{"allow":["new"],"deny":[]}},"allow-remove-by-id":{"identifier":"allow-remove-by-id","description":"Enables the remove_by_id command without any pre-configured scope.","commands":{"allow":["remove_by_id"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-icon-as-template":{"identifier":"allow-set-icon-as-template","description":"Enables the set_icon_as_template command without any pre-configured scope.","commands":{"allow":["set_icon_as_template"],"deny":[]}},"allow-set-menu":{"identifier":"allow-set-menu","description":"Enables the set_menu command without any pre-configured scope.","commands":{"allow":["set_menu"],"deny":[]}},"allow-set-show-menu-on-left-click":{"identifier":"allow-set-show-menu-on-left-click","description":"Enables the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":["set_show_menu_on_left_click"],"deny":[]}},"allow-set-temp-dir-path":{"identifier":"allow-set-temp-dir-path","description":"Enables the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":["set_temp_dir_path"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-tooltip":{"identifier":"allow-set-tooltip","description":"Enables the set_tooltip command without any pre-configured scope.","commands":{"allow":["set_tooltip"],"deny":[]}},"allow-set-visible":{"identifier":"allow-set-visible","description":"Enables the set_visible command without any pre-configured scope.","commands":{"allow":["set_visible"],"deny":[]}},"deny-get-by-id":{"identifier":"deny-get-by-id","description":"Denies the get_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["get_by_id"]}},"deny-new":{"identifier":"deny-new","description":"Denies the new command without any pre-configured scope.","commands":{"allow":[],"deny":["new"]}},"deny-remove-by-id":{"identifier":"deny-remove-by-id","description":"Denies the remove_by_id command without any pre-configured scope.","commands":{"allow":[],"deny":["remove_by_id"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-icon-as-template":{"identifier":"deny-set-icon-as-template","description":"Denies the set_icon_as_template command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon_as_template"]}},"deny-set-menu":{"identifier":"deny-set-menu","description":"Denies the set_menu command without any pre-configured scope.","commands":{"allow":[],"deny":["set_menu"]}},"deny-set-show-menu-on-left-click":{"identifier":"deny-set-show-menu-on-left-click","description":"Denies the set_show_menu_on_left_click command without any pre-configured scope.","commands":{"allow":[],"deny":["set_show_menu_on_left_click"]}},"deny-set-temp-dir-path":{"identifier":"deny-set-temp-dir-path","description":"Denies the set_temp_dir_path command without any pre-configured scope.","commands":{"allow":[],"deny":["set_temp_dir_path"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-tooltip":{"identifier":"deny-set-tooltip","description":"Denies the set_tooltip command without any pre-configured scope.","commands":{"allow":[],"deny":["set_tooltip"]}},"deny-set-visible":{"identifier":"deny-set-visible","description":"Denies the set_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible"]}}},"permission_sets":{},"global_scope_schema":null},"core:webview":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-webviews","allow-webview-position","allow-webview-size","allow-internal-toggle-devtools"]},"permissions":{"allow-clear-all-browsing-data":{"identifier":"allow-clear-all-browsing-data","description":"Enables the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":["clear_all_browsing_data"],"deny":[]}},"allow-create-webview":{"identifier":"allow-create-webview","description":"Enables the create_webview command without any pre-configured scope.","commands":{"allow":["create_webview"],"deny":[]}},"allow-create-webview-window":{"identifier":"allow-create-webview-window","description":"Enables the create_webview_window command without any pre-configured scope.","commands":{"allow":["create_webview_window"],"deny":[]}},"allow-get-all-webviews":{"identifier":"allow-get-all-webviews","description":"Enables the get_all_webviews command without any pre-configured scope.","commands":{"allow":["get_all_webviews"],"deny":[]}},"allow-internal-toggle-devtools":{"identifier":"allow-internal-toggle-devtools","description":"Enables the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":["internal_toggle_devtools"],"deny":[]}},"allow-print":{"identifier":"allow-print","description":"Enables the print command without any pre-configured scope.","commands":{"allow":["print"],"deny":[]}},"allow-reparent":{"identifier":"allow-reparent","description":"Enables the reparent command without any pre-configured scope.","commands":{"allow":["reparent"],"deny":[]}},"allow-set-webview-auto-resize":{"identifier":"allow-set-webview-auto-resize","description":"Enables the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":["set_webview_auto_resize"],"deny":[]}},"allow-set-webview-background-color":{"identifier":"allow-set-webview-background-color","description":"Enables the set_webview_background_color command without any pre-configured scope.","commands":{"allow":["set_webview_background_color"],"deny":[]}},"allow-set-webview-focus":{"identifier":"allow-set-webview-focus","description":"Enables the set_webview_focus command without any pre-configured scope.","commands":{"allow":["set_webview_focus"],"deny":[]}},"allow-set-webview-position":{"identifier":"allow-set-webview-position","description":"Enables the set_webview_position command without any pre-configured scope.","commands":{"allow":["set_webview_position"],"deny":[]}},"allow-set-webview-size":{"identifier":"allow-set-webview-size","description":"Enables the set_webview_size command without any pre-configured scope.","commands":{"allow":["set_webview_size"],"deny":[]}},"allow-set-webview-zoom":{"identifier":"allow-set-webview-zoom","description":"Enables the set_webview_zoom command without any pre-configured scope.","commands":{"allow":["set_webview_zoom"],"deny":[]}},"allow-webview-close":{"identifier":"allow-webview-close","description":"Enables the webview_close command without any pre-configured scope.","commands":{"allow":["webview_close"],"deny":[]}},"allow-webview-hide":{"identifier":"allow-webview-hide","description":"Enables the webview_hide command without any pre-configured scope.","commands":{"allow":["webview_hide"],"deny":[]}},"allow-webview-position":{"identifier":"allow-webview-position","description":"Enables the webview_position command without any pre-configured scope.","commands":{"allow":["webview_position"],"deny":[]}},"allow-webview-show":{"identifier":"allow-webview-show","description":"Enables the webview_show command without any pre-configured scope.","commands":{"allow":["webview_show"],"deny":[]}},"allow-webview-size":{"identifier":"allow-webview-size","description":"Enables the webview_size command without any pre-configured scope.","commands":{"allow":["webview_size"],"deny":[]}},"deny-clear-all-browsing-data":{"identifier":"deny-clear-all-browsing-data","description":"Denies the clear_all_browsing_data command without any pre-configured scope.","commands":{"allow":[],"deny":["clear_all_browsing_data"]}},"deny-create-webview":{"identifier":"deny-create-webview","description":"Denies the create_webview command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview"]}},"deny-create-webview-window":{"identifier":"deny-create-webview-window","description":"Denies the create_webview_window command without any pre-configured scope.","commands":{"allow":[],"deny":["create_webview_window"]}},"deny-get-all-webviews":{"identifier":"deny-get-all-webviews","description":"Denies the get_all_webviews command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_webviews"]}},"deny-internal-toggle-devtools":{"identifier":"deny-internal-toggle-devtools","description":"Denies the internal_toggle_devtools command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_devtools"]}},"deny-print":{"identifier":"deny-print","description":"Denies the print command without any pre-configured scope.","commands":{"allow":[],"deny":["print"]}},"deny-reparent":{"identifier":"deny-reparent","description":"Denies the reparent command without any pre-configured scope.","commands":{"allow":[],"deny":["reparent"]}},"deny-set-webview-auto-resize":{"identifier":"deny-set-webview-auto-resize","description":"Denies the set_webview_auto_resize command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_auto_resize"]}},"deny-set-webview-background-color":{"identifier":"deny-set-webview-background-color","description":"Denies the set_webview_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_background_color"]}},"deny-set-webview-focus":{"identifier":"deny-set-webview-focus","description":"Denies the set_webview_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_focus"]}},"deny-set-webview-position":{"identifier":"deny-set-webview-position","description":"Denies the set_webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_position"]}},"deny-set-webview-size":{"identifier":"deny-set-webview-size","description":"Denies the set_webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_size"]}},"deny-set-webview-zoom":{"identifier":"deny-set-webview-zoom","description":"Denies the set_webview_zoom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_webview_zoom"]}},"deny-webview-close":{"identifier":"deny-webview-close","description":"Denies the webview_close command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_close"]}},"deny-webview-hide":{"identifier":"deny-webview-hide","description":"Denies the webview_hide command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_hide"]}},"deny-webview-position":{"identifier":"deny-webview-position","description":"Denies the webview_position command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_position"]}},"deny-webview-show":{"identifier":"deny-webview-show","description":"Denies the webview_show command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_show"]}},"deny-webview-size":{"identifier":"deny-webview-size","description":"Denies the webview_size command without any pre-configured scope.","commands":{"allow":[],"deny":["webview_size"]}}},"permission_sets":{},"global_scope_schema":null},"core:window":{"default_permission":{"identifier":"default","description":"Default permissions for the plugin.","permissions":["allow-get-all-windows","allow-scale-factor","allow-inner-position","allow-outer-position","allow-inner-size","allow-outer-size","allow-is-fullscreen","allow-is-minimized","allow-is-maximized","allow-is-focused","allow-is-decorated","allow-is-resizable","allow-is-maximizable","allow-is-minimizable","allow-is-closable","allow-is-visible","allow-is-enabled","allow-title","allow-current-monitor","allow-primary-monitor","allow-monitor-from-point","allow-available-monitors","allow-cursor-position","allow-theme","allow-is-always-on-top","allow-internal-toggle-maximize"]},"permissions":{"allow-available-monitors":{"identifier":"allow-available-monitors","description":"Enables the available_monitors command without any pre-configured scope.","commands":{"allow":["available_monitors"],"deny":[]}},"allow-center":{"identifier":"allow-center","description":"Enables the center command without any pre-configured scope.","commands":{"allow":["center"],"deny":[]}},"allow-close":{"identifier":"allow-close","description":"Enables the close command without any pre-configured scope.","commands":{"allow":["close"],"deny":[]}},"allow-create":{"identifier":"allow-create","description":"Enables the create command without any pre-configured scope.","commands":{"allow":["create"],"deny":[]}},"allow-current-monitor":{"identifier":"allow-current-monitor","description":"Enables the current_monitor command without any pre-configured scope.","commands":{"allow":["current_monitor"],"deny":[]}},"allow-cursor-position":{"identifier":"allow-cursor-position","description":"Enables the cursor_position command without any pre-configured scope.","commands":{"allow":["cursor_position"],"deny":[]}},"allow-destroy":{"identifier":"allow-destroy","description":"Enables the destroy command without any pre-configured scope.","commands":{"allow":["destroy"],"deny":[]}},"allow-get-all-windows":{"identifier":"allow-get-all-windows","description":"Enables the get_all_windows command without any pre-configured scope.","commands":{"allow":["get_all_windows"],"deny":[]}},"allow-hide":{"identifier":"allow-hide","description":"Enables the hide command without any pre-configured scope.","commands":{"allow":["hide"],"deny":[]}},"allow-inner-position":{"identifier":"allow-inner-position","description":"Enables the inner_position command without any pre-configured scope.","commands":{"allow":["inner_position"],"deny":[]}},"allow-inner-size":{"identifier":"allow-inner-size","description":"Enables the inner_size command without any pre-configured scope.","commands":{"allow":["inner_size"],"deny":[]}},"allow-internal-toggle-maximize":{"identifier":"allow-internal-toggle-maximize","description":"Enables the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":["internal_toggle_maximize"],"deny":[]}},"allow-is-always-on-top":{"identifier":"allow-is-always-on-top","description":"Enables the is_always_on_top command without any pre-configured scope.","commands":{"allow":["is_always_on_top"],"deny":[]}},"allow-is-closable":{"identifier":"allow-is-closable","description":"Enables the is_closable command without any pre-configured scope.","commands":{"allow":["is_closable"],"deny":[]}},"allow-is-decorated":{"identifier":"allow-is-decorated","description":"Enables the is_decorated command without any pre-configured scope.","commands":{"allow":["is_decorated"],"deny":[]}},"allow-is-enabled":{"identifier":"allow-is-enabled","description":"Enables the is_enabled command without any pre-configured scope.","commands":{"allow":["is_enabled"],"deny":[]}},"allow-is-focused":{"identifier":"allow-is-focused","description":"Enables the is_focused command without any pre-configured scope.","commands":{"allow":["is_focused"],"deny":[]}},"allow-is-fullscreen":{"identifier":"allow-is-fullscreen","description":"Enables the is_fullscreen command without any pre-configured scope.","commands":{"allow":["is_fullscreen"],"deny":[]}},"allow-is-maximizable":{"identifier":"allow-is-maximizable","description":"Enables the is_maximizable command without any pre-configured scope.","commands":{"allow":["is_maximizable"],"deny":[]}},"allow-is-maximized":{"identifier":"allow-is-maximized","description":"Enables the is_maximized command without any pre-configured scope.","commands":{"allow":["is_maximized"],"deny":[]}},"allow-is-minimizable":{"identifier":"allow-is-minimizable","description":"Enables the is_minimizable command without any pre-configured scope.","commands":{"allow":["is_minimizable"],"deny":[]}},"allow-is-minimized":{"identifier":"allow-is-minimized","description":"Enables the is_minimized command without any pre-configured scope.","commands":{"allow":["is_minimized"],"deny":[]}},"allow-is-resizable":{"identifier":"allow-is-resizable","description":"Enables the is_resizable command without any pre-configured scope.","commands":{"allow":["is_resizable"],"deny":[]}},"allow-is-visible":{"identifier":"allow-is-visible","description":"Enables the is_visible command without any pre-configured scope.","commands":{"allow":["is_visible"],"deny":[]}},"allow-maximize":{"identifier":"allow-maximize","description":"Enables the maximize command without any pre-configured scope.","commands":{"allow":["maximize"],"deny":[]}},"allow-minimize":{"identifier":"allow-minimize","description":"Enables the minimize command without any pre-configured scope.","commands":{"allow":["minimize"],"deny":[]}},"allow-monitor-from-point":{"identifier":"allow-monitor-from-point","description":"Enables the monitor_from_point command without any pre-configured scope.","commands":{"allow":["monitor_from_point"],"deny":[]}},"allow-outer-position":{"identifier":"allow-outer-position","description":"Enables the outer_position command without any pre-configured scope.","commands":{"allow":["outer_position"],"deny":[]}},"allow-outer-size":{"identifier":"allow-outer-size","description":"Enables the outer_size command without any pre-configured scope.","commands":{"allow":["outer_size"],"deny":[]}},"allow-primary-monitor":{"identifier":"allow-primary-monitor","description":"Enables the primary_monitor command without any pre-configured scope.","commands":{"allow":["primary_monitor"],"deny":[]}},"allow-request-user-attention":{"identifier":"allow-request-user-attention","description":"Enables the request_user_attention command without any pre-configured scope.","commands":{"allow":["request_user_attention"],"deny":[]}},"allow-scale-factor":{"identifier":"allow-scale-factor","description":"Enables the scale_factor command without any pre-configured scope.","commands":{"allow":["scale_factor"],"deny":[]}},"allow-set-always-on-bottom":{"identifier":"allow-set-always-on-bottom","description":"Enables the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":["set_always_on_bottom"],"deny":[]}},"allow-set-always-on-top":{"identifier":"allow-set-always-on-top","description":"Enables the set_always_on_top command without any pre-configured scope.","commands":{"allow":["set_always_on_top"],"deny":[]}},"allow-set-background-color":{"identifier":"allow-set-background-color","description":"Enables the set_background_color command without any pre-configured scope.","commands":{"allow":["set_background_color"],"deny":[]}},"allow-set-badge-count":{"identifier":"allow-set-badge-count","description":"Enables the set_badge_count command without any pre-configured scope.","commands":{"allow":["set_badge_count"],"deny":[]}},"allow-set-badge-label":{"identifier":"allow-set-badge-label","description":"Enables the set_badge_label command without any pre-configured scope.","commands":{"allow":["set_badge_label"],"deny":[]}},"allow-set-closable":{"identifier":"allow-set-closable","description":"Enables the set_closable command without any pre-configured scope.","commands":{"allow":["set_closable"],"deny":[]}},"allow-set-content-protected":{"identifier":"allow-set-content-protected","description":"Enables the set_content_protected command without any pre-configured scope.","commands":{"allow":["set_content_protected"],"deny":[]}},"allow-set-cursor-grab":{"identifier":"allow-set-cursor-grab","description":"Enables the set_cursor_grab command without any pre-configured scope.","commands":{"allow":["set_cursor_grab"],"deny":[]}},"allow-set-cursor-icon":{"identifier":"allow-set-cursor-icon","description":"Enables the set_cursor_icon command without any pre-configured scope.","commands":{"allow":["set_cursor_icon"],"deny":[]}},"allow-set-cursor-position":{"identifier":"allow-set-cursor-position","description":"Enables the set_cursor_position command without any pre-configured scope.","commands":{"allow":["set_cursor_position"],"deny":[]}},"allow-set-cursor-visible":{"identifier":"allow-set-cursor-visible","description":"Enables the set_cursor_visible command without any pre-configured scope.","commands":{"allow":["set_cursor_visible"],"deny":[]}},"allow-set-decorations":{"identifier":"allow-set-decorations","description":"Enables the set_decorations command without any pre-configured scope.","commands":{"allow":["set_decorations"],"deny":[]}},"allow-set-effects":{"identifier":"allow-set-effects","description":"Enables the set_effects command without any pre-configured scope.","commands":{"allow":["set_effects"],"deny":[]}},"allow-set-enabled":{"identifier":"allow-set-enabled","description":"Enables the set_enabled command without any pre-configured scope.","commands":{"allow":["set_enabled"],"deny":[]}},"allow-set-focus":{"identifier":"allow-set-focus","description":"Enables the set_focus command without any pre-configured scope.","commands":{"allow":["set_focus"],"deny":[]}},"allow-set-focusable":{"identifier":"allow-set-focusable","description":"Enables the set_focusable command without any pre-configured scope.","commands":{"allow":["set_focusable"],"deny":[]}},"allow-set-fullscreen":{"identifier":"allow-set-fullscreen","description":"Enables the set_fullscreen command without any pre-configured scope.","commands":{"allow":["set_fullscreen"],"deny":[]}},"allow-set-icon":{"identifier":"allow-set-icon","description":"Enables the set_icon command without any pre-configured scope.","commands":{"allow":["set_icon"],"deny":[]}},"allow-set-ignore-cursor-events":{"identifier":"allow-set-ignore-cursor-events","description":"Enables the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":["set_ignore_cursor_events"],"deny":[]}},"allow-set-max-size":{"identifier":"allow-set-max-size","description":"Enables the set_max_size command without any pre-configured scope.","commands":{"allow":["set_max_size"],"deny":[]}},"allow-set-maximizable":{"identifier":"allow-set-maximizable","description":"Enables the set_maximizable command without any pre-configured scope.","commands":{"allow":["set_maximizable"],"deny":[]}},"allow-set-min-size":{"identifier":"allow-set-min-size","description":"Enables the set_min_size command without any pre-configured scope.","commands":{"allow":["set_min_size"],"deny":[]}},"allow-set-minimizable":{"identifier":"allow-set-minimizable","description":"Enables the set_minimizable command without any pre-configured scope.","commands":{"allow":["set_minimizable"],"deny":[]}},"allow-set-overlay-icon":{"identifier":"allow-set-overlay-icon","description":"Enables the set_overlay_icon command without any pre-configured scope.","commands":{"allow":["set_overlay_icon"],"deny":[]}},"allow-set-position":{"identifier":"allow-set-position","description":"Enables the set_position command without any pre-configured scope.","commands":{"allow":["set_position"],"deny":[]}},"allow-set-progress-bar":{"identifier":"allow-set-progress-bar","description":"Enables the set_progress_bar command without any pre-configured scope.","commands":{"allow":["set_progress_bar"],"deny":[]}},"allow-set-resizable":{"identifier":"allow-set-resizable","description":"Enables the set_resizable command without any pre-configured scope.","commands":{"allow":["set_resizable"],"deny":[]}},"allow-set-shadow":{"identifier":"allow-set-shadow","description":"Enables the set_shadow command without any pre-configured scope.","commands":{"allow":["set_shadow"],"deny":[]}},"allow-set-simple-fullscreen":{"identifier":"allow-set-simple-fullscreen","description":"Enables the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":["set_simple_fullscreen"],"deny":[]}},"allow-set-size":{"identifier":"allow-set-size","description":"Enables the set_size command without any pre-configured scope.","commands":{"allow":["set_size"],"deny":[]}},"allow-set-size-constraints":{"identifier":"allow-set-size-constraints","description":"Enables the set_size_constraints command without any pre-configured scope.","commands":{"allow":["set_size_constraints"],"deny":[]}},"allow-set-skip-taskbar":{"identifier":"allow-set-skip-taskbar","description":"Enables the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":["set_skip_taskbar"],"deny":[]}},"allow-set-theme":{"identifier":"allow-set-theme","description":"Enables the set_theme command without any pre-configured scope.","commands":{"allow":["set_theme"],"deny":[]}},"allow-set-title":{"identifier":"allow-set-title","description":"Enables the set_title command without any pre-configured scope.","commands":{"allow":["set_title"],"deny":[]}},"allow-set-title-bar-style":{"identifier":"allow-set-title-bar-style","description":"Enables the set_title_bar_style command without any pre-configured scope.","commands":{"allow":["set_title_bar_style"],"deny":[]}},"allow-set-visible-on-all-workspaces":{"identifier":"allow-set-visible-on-all-workspaces","description":"Enables the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":["set_visible_on_all_workspaces"],"deny":[]}},"allow-show":{"identifier":"allow-show","description":"Enables the show command without any pre-configured scope.","commands":{"allow":["show"],"deny":[]}},"allow-start-dragging":{"identifier":"allow-start-dragging","description":"Enables the start_dragging command without any pre-configured scope.","commands":{"allow":["start_dragging"],"deny":[]}},"allow-start-resize-dragging":{"identifier":"allow-start-resize-dragging","description":"Enables the start_resize_dragging command without any pre-configured scope.","commands":{"allow":["start_resize_dragging"],"deny":[]}},"allow-theme":{"identifier":"allow-theme","description":"Enables the theme command without any pre-configured scope.","commands":{"allow":["theme"],"deny":[]}},"allow-title":{"identifier":"allow-title","description":"Enables the title command without any pre-configured scope.","commands":{"allow":["title"],"deny":[]}},"allow-toggle-maximize":{"identifier":"allow-toggle-maximize","description":"Enables the toggle_maximize command without any pre-configured scope.","commands":{"allow":["toggle_maximize"],"deny":[]}},"allow-unmaximize":{"identifier":"allow-unmaximize","description":"Enables the unmaximize command without any pre-configured scope.","commands":{"allow":["unmaximize"],"deny":[]}},"allow-unminimize":{"identifier":"allow-unminimize","description":"Enables the unminimize command without any pre-configured scope.","commands":{"allow":["unminimize"],"deny":[]}},"deny-available-monitors":{"identifier":"deny-available-monitors","description":"Denies the available_monitors command without any pre-configured scope.","commands":{"allow":[],"deny":["available_monitors"]}},"deny-center":{"identifier":"deny-center","description":"Denies the center command without any pre-configured scope.","commands":{"allow":[],"deny":["center"]}},"deny-close":{"identifier":"deny-close","description":"Denies the close command without any pre-configured scope.","commands":{"allow":[],"deny":["close"]}},"deny-create":{"identifier":"deny-create","description":"Denies the create command without any pre-configured scope.","commands":{"allow":[],"deny":["create"]}},"deny-current-monitor":{"identifier":"deny-current-monitor","description":"Denies the current_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["current_monitor"]}},"deny-cursor-position":{"identifier":"deny-cursor-position","description":"Denies the cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["cursor_position"]}},"deny-destroy":{"identifier":"deny-destroy","description":"Denies the destroy command without any pre-configured scope.","commands":{"allow":[],"deny":["destroy"]}},"deny-get-all-windows":{"identifier":"deny-get-all-windows","description":"Denies the get_all_windows command without any pre-configured scope.","commands":{"allow":[],"deny":["get_all_windows"]}},"deny-hide":{"identifier":"deny-hide","description":"Denies the hide command without any pre-configured scope.","commands":{"allow":[],"deny":["hide"]}},"deny-inner-position":{"identifier":"deny-inner-position","description":"Denies the inner_position command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_position"]}},"deny-inner-size":{"identifier":"deny-inner-size","description":"Denies the inner_size command without any pre-configured scope.","commands":{"allow":[],"deny":["inner_size"]}},"deny-internal-toggle-maximize":{"identifier":"deny-internal-toggle-maximize","description":"Denies the internal_toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["internal_toggle_maximize"]}},"deny-is-always-on-top":{"identifier":"deny-is-always-on-top","description":"Denies the is_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["is_always_on_top"]}},"deny-is-closable":{"identifier":"deny-is-closable","description":"Denies the is_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_closable"]}},"deny-is-decorated":{"identifier":"deny-is-decorated","description":"Denies the is_decorated command without any pre-configured scope.","commands":{"allow":[],"deny":["is_decorated"]}},"deny-is-enabled":{"identifier":"deny-is-enabled","description":"Denies the is_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["is_enabled"]}},"deny-is-focused":{"identifier":"deny-is-focused","description":"Denies the is_focused command without any pre-configured scope.","commands":{"allow":[],"deny":["is_focused"]}},"deny-is-fullscreen":{"identifier":"deny-is-fullscreen","description":"Denies the is_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["is_fullscreen"]}},"deny-is-maximizable":{"identifier":"deny-is-maximizable","description":"Denies the is_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximizable"]}},"deny-is-maximized":{"identifier":"deny-is-maximized","description":"Denies the is_maximized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_maximized"]}},"deny-is-minimizable":{"identifier":"deny-is-minimizable","description":"Denies the is_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimizable"]}},"deny-is-minimized":{"identifier":"deny-is-minimized","description":"Denies the is_minimized command without any pre-configured scope.","commands":{"allow":[],"deny":["is_minimized"]}},"deny-is-resizable":{"identifier":"deny-is-resizable","description":"Denies the is_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["is_resizable"]}},"deny-is-visible":{"identifier":"deny-is-visible","description":"Denies the is_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["is_visible"]}},"deny-maximize":{"identifier":"deny-maximize","description":"Denies the maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["maximize"]}},"deny-minimize":{"identifier":"deny-minimize","description":"Denies the minimize command without any pre-configured scope.","commands":{"allow":[],"deny":["minimize"]}},"deny-monitor-from-point":{"identifier":"deny-monitor-from-point","description":"Denies the monitor_from_point command without any pre-configured scope.","commands":{"allow":[],"deny":["monitor_from_point"]}},"deny-outer-position":{"identifier":"deny-outer-position","description":"Denies the outer_position command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_position"]}},"deny-outer-size":{"identifier":"deny-outer-size","description":"Denies the outer_size command without any pre-configured scope.","commands":{"allow":[],"deny":["outer_size"]}},"deny-primary-monitor":{"identifier":"deny-primary-monitor","description":"Denies the primary_monitor command without any pre-configured scope.","commands":{"allow":[],"deny":["primary_monitor"]}},"deny-request-user-attention":{"identifier":"deny-request-user-attention","description":"Denies the request_user_attention command without any pre-configured scope.","commands":{"allow":[],"deny":["request_user_attention"]}},"deny-scale-factor":{"identifier":"deny-scale-factor","description":"Denies the scale_factor command without any pre-configured scope.","commands":{"allow":[],"deny":["scale_factor"]}},"deny-set-always-on-bottom":{"identifier":"deny-set-always-on-bottom","description":"Denies the set_always_on_bottom command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_bottom"]}},"deny-set-always-on-top":{"identifier":"deny-set-always-on-top","description":"Denies the set_always_on_top command without any pre-configured scope.","commands":{"allow":[],"deny":["set_always_on_top"]}},"deny-set-background-color":{"identifier":"deny-set-background-color","description":"Denies the set_background_color command without any pre-configured scope.","commands":{"allow":[],"deny":["set_background_color"]}},"deny-set-badge-count":{"identifier":"deny-set-badge-count","description":"Denies the set_badge_count command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_count"]}},"deny-set-badge-label":{"identifier":"deny-set-badge-label","description":"Denies the set_badge_label command without any pre-configured scope.","commands":{"allow":[],"deny":["set_badge_label"]}},"deny-set-closable":{"identifier":"deny-set-closable","description":"Denies the set_closable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_closable"]}},"deny-set-content-protected":{"identifier":"deny-set-content-protected","description":"Denies the set_content_protected command without any pre-configured scope.","commands":{"allow":[],"deny":["set_content_protected"]}},"deny-set-cursor-grab":{"identifier":"deny-set-cursor-grab","description":"Denies the set_cursor_grab command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_grab"]}},"deny-set-cursor-icon":{"identifier":"deny-set-cursor-icon","description":"Denies the set_cursor_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_icon"]}},"deny-set-cursor-position":{"identifier":"deny-set-cursor-position","description":"Denies the set_cursor_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_position"]}},"deny-set-cursor-visible":{"identifier":"deny-set-cursor-visible","description":"Denies the set_cursor_visible command without any pre-configured scope.","commands":{"allow":[],"deny":["set_cursor_visible"]}},"deny-set-decorations":{"identifier":"deny-set-decorations","description":"Denies the set_decorations command without any pre-configured scope.","commands":{"allow":[],"deny":["set_decorations"]}},"deny-set-effects":{"identifier":"deny-set-effects","description":"Denies the set_effects command without any pre-configured scope.","commands":{"allow":[],"deny":["set_effects"]}},"deny-set-enabled":{"identifier":"deny-set-enabled","description":"Denies the set_enabled command without any pre-configured scope.","commands":{"allow":[],"deny":["set_enabled"]}},"deny-set-focus":{"identifier":"deny-set-focus","description":"Denies the set_focus command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focus"]}},"deny-set-focusable":{"identifier":"deny-set-focusable","description":"Denies the set_focusable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_focusable"]}},"deny-set-fullscreen":{"identifier":"deny-set-fullscreen","description":"Denies the set_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_fullscreen"]}},"deny-set-icon":{"identifier":"deny-set-icon","description":"Denies the set_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_icon"]}},"deny-set-ignore-cursor-events":{"identifier":"deny-set-ignore-cursor-events","description":"Denies the set_ignore_cursor_events command without any pre-configured scope.","commands":{"allow":[],"deny":["set_ignore_cursor_events"]}},"deny-set-max-size":{"identifier":"deny-set-max-size","description":"Denies the set_max_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_max_size"]}},"deny-set-maximizable":{"identifier":"deny-set-maximizable","description":"Denies the set_maximizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_maximizable"]}},"deny-set-min-size":{"identifier":"deny-set-min-size","description":"Denies the set_min_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_min_size"]}},"deny-set-minimizable":{"identifier":"deny-set-minimizable","description":"Denies the set_minimizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_minimizable"]}},"deny-set-overlay-icon":{"identifier":"deny-set-overlay-icon","description":"Denies the set_overlay_icon command without any pre-configured scope.","commands":{"allow":[],"deny":["set_overlay_icon"]}},"deny-set-position":{"identifier":"deny-set-position","description":"Denies the set_position command without any pre-configured scope.","commands":{"allow":[],"deny":["set_position"]}},"deny-set-progress-bar":{"identifier":"deny-set-progress-bar","description":"Denies the set_progress_bar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_progress_bar"]}},"deny-set-resizable":{"identifier":"deny-set-resizable","description":"Denies the set_resizable command without any pre-configured scope.","commands":{"allow":[],"deny":["set_resizable"]}},"deny-set-shadow":{"identifier":"deny-set-shadow","description":"Denies the set_shadow command without any pre-configured scope.","commands":{"allow":[],"deny":["set_shadow"]}},"deny-set-simple-fullscreen":{"identifier":"deny-set-simple-fullscreen","description":"Denies the set_simple_fullscreen command without any pre-configured scope.","commands":{"allow":[],"deny":["set_simple_fullscreen"]}},"deny-set-size":{"identifier":"deny-set-size","description":"Denies the set_size command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size"]}},"deny-set-size-constraints":{"identifier":"deny-set-size-constraints","description":"Denies the set_size_constraints command without any pre-configured scope.","commands":{"allow":[],"deny":["set_size_constraints"]}},"deny-set-skip-taskbar":{"identifier":"deny-set-skip-taskbar","description":"Denies the set_skip_taskbar command without any pre-configured scope.","commands":{"allow":[],"deny":["set_skip_taskbar"]}},"deny-set-theme":{"identifier":"deny-set-theme","description":"Denies the set_theme command without any pre-configured scope.","commands":{"allow":[],"deny":["set_theme"]}},"deny-set-title":{"identifier":"deny-set-title","description":"Denies the set_title command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title"]}},"deny-set-title-bar-style":{"identifier":"deny-set-title-bar-style","description":"Denies the set_title_bar_style command without any pre-configured scope.","commands":{"allow":[],"deny":["set_title_bar_style"]}},"deny-set-visible-on-all-workspaces":{"identifier":"deny-set-visible-on-all-workspaces","description":"Denies the set_visible_on_all_workspaces command without any pre-configured scope.","commands":{"allow":[],"deny":["set_visible_on_all_workspaces"]}},"deny-show":{"identifier":"deny-show","description":"Denies the show command without any pre-configured scope.","commands":{"allow":[],"deny":["show"]}},"deny-start-dragging":{"identifier":"deny-start-dragging","description":"Denies the start_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_dragging"]}},"deny-start-resize-dragging":{"identifier":"deny-start-resize-dragging","description":"Denies the start_resize_dragging command without any pre-configured scope.","commands":{"allow":[],"deny":["start_resize_dragging"]}},"deny-theme":{"identifier":"deny-theme","description":"Denies the theme command without any pre-configured scope.","commands":{"allow":[],"deny":["theme"]}},"deny-title":{"identifier":"deny-title","description":"Denies the title command without any pre-configured scope.","commands":{"allow":[],"deny":["title"]}},"deny-toggle-maximize":{"identifier":"deny-toggle-maximize","description":"Denies the toggle_maximize command without any pre-configured scope.","commands":{"allow":[],"deny":["toggle_maximize"]}},"deny-unmaximize":{"identifier":"deny-unmaximize","description":"Denies the unmaximize command without any pre-configured scope.","commands":{"allow":[],"deny":["unmaximize"]}},"deny-unminimize":{"identifier":"deny-unminimize","description":"Denies the unminimize command without any pre-configured scope.","commands":{"allow":[],"deny":["unminimize"]}}},"permission_sets":{},"global_scope_schema":null},"dialog":{"default_permission":{"identifier":"default","description":"This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n","permissions":["allow-ask","allow-confirm","allow-message","allow-save","allow-open"]},"permissions":{"allow-ask":{"identifier":"allow-ask","description":"Enables the ask command without any pre-configured scope.","commands":{"allow":["ask"],"deny":[]}},"allow-confirm":{"identifier":"allow-confirm","description":"Enables the confirm command without any pre-configured scope.","commands":{"allow":["confirm"],"deny":[]}},"allow-message":{"identifier":"allow-message","description":"Enables the message command without any pre-configured scope.","commands":{"allow":["message"],"deny":[]}},"allow-open":{"identifier":"allow-open","description":"Enables the open command without any pre-configured scope.","commands":{"allow":["open"],"deny":[]}},"allow-save":{"identifier":"allow-save","description":"Enables the save command without any pre-configured scope.","commands":{"allow":["save"],"deny":[]}},"deny-ask":{"identifier":"deny-ask","description":"Denies the ask command without any pre-configured scope.","commands":{"allow":[],"deny":["ask"]}},"deny-confirm":{"identifier":"deny-confirm","description":"Denies the confirm command without any pre-configured scope.","commands":{"allow":[],"deny":["confirm"]}},"deny-message":{"identifier":"deny-message","description":"Denies the message command without any pre-configured scope.","commands":{"allow":[],"deny":["message"]}},"deny-open":{"identifier":"deny-open","description":"Denies the open command without any pre-configured scope.","commands":{"allow":[],"deny":["open"]}},"deny-save":{"identifier":"deny-save","description":"Denies the save command without any pre-configured scope.","commands":{"allow":[],"deny":["save"]}}},"permission_sets":{},"global_scope_schema":null},"global-shortcut":{"default_permission":{"identifier":"default","description":"No features are enabled by default, as we believe\nthe shortcuts can be inherently dangerous and it is\napplication specific if specific shortcuts should be\nregistered or unregistered.\n","permissions":[]},"permissions":{"allow-is-registered":{"identifier":"allow-is-registered","description":"Enables the is_registered command without any pre-configured scope.","commands":{"allow":["is_registered"],"deny":[]}},"allow-register":{"identifier":"allow-register","description":"Enables the register command without any pre-configured scope.","commands":{"allow":["register"],"deny":[]}},"allow-register-all":{"identifier":"allow-register-all","description":"Enables the register_all command without any pre-configured scope.","commands":{"allow":["register_all"],"deny":[]}},"allow-unregister":{"identifier":"allow-unregister","description":"Enables the unregister command without any pre-configured scope.","commands":{"allow":["unregister"],"deny":[]}},"allow-unregister-all":{"identifier":"allow-unregister-all","description":"Enables the unregister_all command without any pre-configured scope.","commands":{"allow":["unregister_all"],"deny":[]}},"deny-is-registered":{"identifier":"deny-is-registered","description":"Denies the is_registered command without any pre-configured scope.","commands":{"allow":[],"deny":["is_registered"]}},"deny-register":{"identifier":"deny-register","description":"Denies the register command without any pre-configured scope.","commands":{"allow":[],"deny":["register"]}},"deny-register-all":{"identifier":"deny-register-all","description":"Denies the register_all command without any pre-configured scope.","commands":{"allow":[],"deny":["register_all"]}},"deny-unregister":{"identifier":"deny-unregister","description":"Denies the unregister command without any pre-configured scope.","commands":{"allow":[],"deny":["unregister"]}},"deny-unregister-all":{"identifier":"deny-unregister-all","description":"Denies the unregister_all command without any pre-configured scope.","commands":{"allow":[],"deny":["unregister_all"]}}},"permission_sets":{},"global_scope_schema":null},"opener":{"default_permission":{"identifier":"default","description":"This permission set allows opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application\nas well as reveal file in directories using default file explorer","permissions":["allow-open-url","allow-reveal-item-in-dir","allow-default-urls"]},"permissions":{"allow-default-urls":{"identifier":"allow-default-urls","description":"This enables opening `mailto:`, `tel:`, `https://` and `http://` urls using their default application.","commands":{"allow":[],"deny":[]},"scope":{"allow":[{"url":"mailto:*"},{"url":"tel:*"},{"url":"http://*"},{"url":"https://*"}]}},"allow-open-path":{"identifier":"allow-open-path","description":"Enables the open_path command without any pre-configured scope.","commands":{"allow":["open_path"],"deny":[]}},"allow-open-url":{"identifier":"allow-open-url","description":"Enables the open_url command without any pre-configured scope.","commands":{"allow":["open_url"],"deny":[]}},"allow-reveal-item-in-dir":{"identifier":"allow-reveal-item-in-dir","description":"Enables the reveal_item_in_dir command without any pre-configured scope.","commands":{"allow":["reveal_item_in_dir"],"deny":[]}},"deny-open-path":{"identifier":"deny-open-path","description":"Denies the open_path command without any pre-configured scope.","commands":{"allow":[],"deny":["open_path"]}},"deny-open-url":{"identifier":"deny-open-url","description":"Denies the open_url command without any pre-configured scope.","commands":{"allow":[],"deny":["open_url"]}},"deny-reveal-item-in-dir":{"identifier":"deny-reveal-item-in-dir","description":"Denies the reveal_item_in_dir command without any pre-configured scope.","commands":{"allow":[],"deny":["reveal_item_in_dir"]}}},"permission_sets":{},"global_scope_schema":{"$schema":"http://json-schema.org/draft-07/schema#","anyOf":[{"properties":{"app":{"allOf":[{"$ref":"#/definitions/Application"}],"description":"An application to open this url with, for example: firefox."},"url":{"description":"A URL that can be opened by the webview when using the Opener APIs.\n\nWildcards can be used following the UNIX glob pattern.\n\nExamples:\n\n- \"https://*\" : allows all HTTPS origin\n\n- \"https://*.github.com/tauri-apps/tauri\": allows any subdomain of \"github.com\" with the \"tauri-apps/api\" path\n\n- \"https://myapi.service.com/users/*\": allows access to any URLs that begins with \"https://myapi.service.com/users/\"","type":"string"}},"required":["url"],"type":"object"},{"properties":{"app":{"allOf":[{"$ref":"#/definitions/Application"}],"description":"An application to open this path with, for example: xdg-open."},"path":{"description":"A path that can be opened by the webview when using the Opener APIs.\n\nThe pattern can start with a variable that resolves to a system base directory. The variables are: `$AUDIO`, `$CACHE`, `$CONFIG`, `$DATA`, `$LOCALDATA`, `$DESKTOP`, `$DOCUMENT`, `$DOWNLOAD`, `$EXE`, `$FONT`, `$HOME`, `$PICTURE`, `$PUBLIC`, `$RUNTIME`, `$TEMPLATE`, `$VIDEO`, `$RESOURCE`, `$APP`, `$LOG`, `$TEMP`, `$APPCONFIG`, `$APPDATA`, `$APPLOCALDATA`, `$APPCACHE`, `$APPLOG`.","type":"string"}},"required":["path"],"type":"object"}],"definitions":{"Application":{"anyOf":[{"description":"Open in default application.","type":"null"},{"description":"If true, allow open with any application.","type":"boolean"},{"description":"Allow specific application to open with.","type":"string"}],"description":"Opener scope application."}},"description":"Opener scope entry.","title":"OpenerScopeEntry"}},"updater":{"default_permission":{"identifier":"default","description":"This permission set configures which kind of\nupdater functions are exposed to the frontend.\n\n#### Granted Permissions\n\nThe full workflow from checking for updates to installing them\nis enabled.\n\n","permissions":["allow-check","allow-download","allow-install","allow-download-and-install"]},"permissions":{"allow-check":{"identifier":"allow-check","description":"Enables the check command without any pre-configured scope.","commands":{"allow":["check"],"deny":[]}},"allow-download":{"identifier":"allow-download","description":"Enables the download command without any pre-configured scope.","commands":{"allow":["download"],"deny":[]}},"allow-download-and-install":{"identifier":"allow-download-and-install","description":"Enables the download_and_install command without any pre-configured scope.","commands":{"allow":["download_and_install"],"deny":[]}},"allow-install":{"identifier":"allow-install","description":"Enables the install command without any pre-configured scope.","commands":{"allow":["install"],"deny":[]}},"deny-check":{"identifier":"deny-check","description":"Denies the check command without any pre-configured scope.","commands":{"allow":[],"deny":["check"]}},"deny-download":{"identifier":"deny-download","description":"Denies the download command without any pre-configured scope.","commands":{"allow":[],"deny":["download"]}},"deny-download-and-install":{"identifier":"deny-download-and-install","description":"Denies the download_and_install command without any pre-configured scope.","commands":{"allow":[],"deny":["download_and_install"]}},"deny-install":{"identifier":"deny-install","description":"Denies the install command without any pre-configured scope.","commands":{"allow":[],"deny":["install"]}}},"permission_sets":{},"global_scope_schema":null},"window-state":{"default_permission":{"identifier":"default","description":"This permission set configures what kind of\noperations are available from the window state plugin.\n\n#### Granted Permissions\n\nAll operations are enabled by default.\n\n","permissions":["allow-filename","allow-restore-state","allow-save-window-state"]},"permissions":{"allow-filename":{"identifier":"allow-filename","description":"Enables the filename command without any pre-configured scope.","commands":{"allow":["filename"],"deny":[]}},"allow-restore-state":{"identifier":"allow-restore-state","description":"Enables the restore_state command without any pre-configured scope.","commands":{"allow":["restore_state"],"deny":[]}},"allow-save-window-state":{"identifier":"allow-save-window-state","description":"Enables the save_window_state command without any pre-configured scope.","commands":{"allow":["save_window_state"],"deny":[]}},"deny-filename":{"identifier":"deny-filename","description":"Denies the filename command without any pre-configured scope.","commands":{"allow":[],"deny":["filename"]}},"deny-restore-state":{"identifier":"deny-restore-state","description":"Denies the restore_state command without any pre-configured scope.","commands":{"allow":[],"deny":["restore_state"]}},"deny-save-window-state":{"identifier":"deny-save-window-state","description":"Denies the save_window_state command without any pre-configured scope.","commands":{"allow":[],"deny":["save_window_state"]}}},"permission_sets":{},"global_scope_schema":null}} \ No newline at end of file diff --git a/src-tauri/gen/schemas/desktop-schema.json b/src-tauri/gen/schemas/desktop-schema.json index 831639a..ebcb1b3 100644 --- a/src-tauri/gen/schemas/desktop-schema.json +++ b/src-tauri/gen/schemas/desktop-schema.json @@ -2313,22 +2313,22 @@ "markdownDescription": "Denies the unminimize command without any pre-configured scope." }, { - "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`", "type": "string", "const": "dialog:default", - "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`" }, { - "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "description": "Enables the ask command without any pre-configured scope.", "type": "string", "const": "dialog:allow-ask", - "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + "markdownDescription": "Enables the ask command without any pre-configured scope." }, { - "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "description": "Enables the confirm command without any pre-configured scope.", "type": "string", "const": "dialog:allow-confirm", - "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + "markdownDescription": "Enables the confirm command without any pre-configured scope." }, { "description": "Enables the message command without any pre-configured scope.", @@ -2349,16 +2349,16 @@ "markdownDescription": "Enables the save command without any pre-configured scope." }, { - "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "description": "Denies the ask command without any pre-configured scope.", "type": "string", "const": "dialog:deny-ask", - "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + "markdownDescription": "Denies the ask command without any pre-configured scope." }, { - "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "description": "Denies the confirm command without any pre-configured scope.", "type": "string", "const": "dialog:deny-confirm", - "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + "markdownDescription": "Denies the confirm command without any pre-configured scope." }, { "description": "Denies the message command without any pre-configured scope.", diff --git a/src-tauri/gen/schemas/linux-schema.json b/src-tauri/gen/schemas/linux-schema.json index 831639a..ebcb1b3 100644 --- a/src-tauri/gen/schemas/linux-schema.json +++ b/src-tauri/gen/schemas/linux-schema.json @@ -2313,22 +2313,22 @@ "markdownDescription": "Denies the unminimize command without any pre-configured scope." }, { - "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`", + "description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`", "type": "string", "const": "dialog:default", - "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`" + "markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-ask`\n- `allow-confirm`\n- `allow-message`\n- `allow-save`\n- `allow-open`" }, { - "description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "description": "Enables the ask command without any pre-configured scope.", "type": "string", "const": "dialog:allow-ask", - "markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + "markdownDescription": "Enables the ask command without any pre-configured scope." }, { - "description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)", + "description": "Enables the confirm command without any pre-configured scope.", "type": "string", "const": "dialog:allow-confirm", - "markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)" + "markdownDescription": "Enables the confirm command without any pre-configured scope." }, { "description": "Enables the message command without any pre-configured scope.", @@ -2349,16 +2349,16 @@ "markdownDescription": "Enables the save command without any pre-configured scope." }, { - "description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "description": "Denies the ask command without any pre-configured scope.", "type": "string", "const": "dialog:deny-ask", - "markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + "markdownDescription": "Denies the ask command without any pre-configured scope." }, { - "description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)", + "description": "Denies the confirm command without any pre-configured scope.", "type": "string", "const": "dialog:deny-confirm", - "markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)" + "markdownDescription": "Denies the confirm command without any pre-configured scope." }, { "description": "Denies the message command without any pre-configured scope.", diff --git a/src-tauri/src/commands/audio.rs b/src-tauri/src/commands/audio.rs index f7b3360..e89535a 100644 --- a/src-tauri/src/commands/audio.rs +++ b/src-tauri/src/commands/audio.rs @@ -308,8 +308,7 @@ fn recording_filename() -> String { /// restarts, so cross-launch collisions are already impossible — the /// counter is the last-mile guarantee against within-launch same-tick /// collisions. -static RECORDING_COUNTER: std::sync::atomic::AtomicU64 = - std::sync::atomic::AtomicU64::new(0); +static RECORDING_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); #[cfg(test)] mod tests { @@ -353,10 +352,20 @@ mod tests { 3, "expected three '-' separated parts, got {parts:?}" ); - assert!(parts[0].chars().all(|c| c.is_ascii_digit()), "secs is digits"); - assert_eq!(parts[1].len(), 9, "nanos component is zero-padded to 9 digits"); + assert!( + parts[0].chars().all(|c| c.is_ascii_digit()), + "secs is digits" + ); + assert_eq!( + parts[1].len(), + 9, + "nanos component is zero-padded to 9 digits" + ); assert!(parts[1].chars().all(|c| c.is_ascii_digit())); - assert!(parts[2].len() >= 4, "counter component is zero-padded to >=4 digits"); + assert!( + parts[2].len() >= 4, + "counter component is zero-padded to >=4 digits" + ); assert!(parts[2].chars().all(|c| c.is_ascii_digit())); } diff --git a/src-tauri/src/commands/diagnostics.rs b/src-tauri/src/commands/diagnostics.rs index 5d77f9f..87cc64a 100644 --- a/src-tauri/src/commands/diagnostics.rs +++ b/src-tauri/src/commands/diagnostics.rs @@ -19,6 +19,7 @@ use kon_storage::{ app_data_dir, crashes_dir, list_recent_errors, log_error, logs_dir, ErrorLogRow, }; +use crate::commands::power::active_assertions_snapshot; use crate::AppState; const DEFAULT_RECENT_ERRORS: i64 = 50; @@ -290,6 +291,20 @@ pub async fn generate_diagnostic_report( } } + out.push_str("## Power assertions\n\n"); + let power_assertions = active_assertions_snapshot(); + if power_assertions.is_empty() { + out.push_str("_(no active power assertions at report time)_\n\n"); + } else { + for assertion in power_assertions { + out.push_str(&format!( + "- `#{}` reason=`{}` backend=`{}` acquired=`{}`\n", + assertion.id, assertion.reason, assertion.backend, assertion.acquired + )); + } + out.push('\n'); + } + if opts.include_crashes { out.push_str("## Crash dumps\n\n"); let crashes = list_crash_files().await.unwrap_or_default(); diff --git a/src-tauri/src/commands/live.rs b/src-tauri/src/commands/live.rs index a7f818e..3b3977f 100644 --- a/src-tauri/src/commands/live.rs +++ b/src-tauri/src/commands/live.rs @@ -11,6 +11,7 @@ use std::time::{Duration, Instant}; use serde::{Deserialize, Serialize}; use tauri::ipc::Channel; +use tokio::sync::Mutex as AsyncMutex; use crate::commands::audio::resolve_recording_path; use crate::commands::build_initial_prompt; @@ -18,7 +19,9 @@ use crate::commands::models::{default_model_id_for_engine, ensure_model_loaded}; use crate::commands::power::PowerAssertion; use crate::AppState; use kon_ai_formatting::{post_process_segments, FormatMode, PostProcessOptions}; -use kon_audio::{MicrophoneCapture, StreamingResampler, WavWriter}; +use kon_audio::{ + AudioChunk, CaptureRuntimeError, MicrophoneCapture, StreamingResampler, WavWriter, +}; use kon_core::constants::WHISPER_SAMPLE_RATE; use kon_core::types::{AudioSamples, Segment, TranscriptionOptions}; use kon_transcription::LocalEngine; @@ -57,6 +60,7 @@ const LOW_SIGNAL_TOKENS: &[&str] = &[ #[derive(Default)] pub struct LiveTranscriptionState { next_session_id: AtomicU64, + lifecycle: AsyncMutex<()>, running: Mutex>, } @@ -153,6 +157,292 @@ struct LiveSessionSummary { audio_path: Option, } +/// Session worker state is thread-confined to the single blocking task spawned +/// by `start_live_transcription_session`. Cross-thread coordination happens via +/// the stop flag and mpsc channels only; RB-01 will tighten the outer +/// `live_state.running` lock discipline now that this worker lifecycle is +/// explicit and locally structured. +struct ActiveCapture { + /// Keeping the capture handle alive keeps the underlying cpal stream alive. + _capture: MicrophoneCapture, + rx: std::sync::mpsc::Receiver, + mic_error_rx: Option>, +} + +impl ActiveCapture { + fn start(config: &StartLiveTranscriptionConfig) -> Result { + let (mut capture, rx) = match config.microphone_device.as_deref() { + Some(name) if !name.is_empty() => MicrophoneCapture::start_with_device(name), + _ => MicrophoneCapture::start(), + } + .map_err(|e| e.to_string())?; + let mic_error_rx = capture.take_error_rx(); + Ok(Self { + _capture: capture, + rx, + mic_error_rx, + }) + } + + fn drain_runtime_errors( + &mut self, + session_id: u64, + status_channel: &Channel, + ) { + let Some(err_rx) = &self.mic_error_rx else { + return; + }; + while let Ok(err) = err_rx.try_recv() { + let _ = status_channel.send(LiveStatusMessage::Warning { + session_id, + message: format!( + "Microphone '{}' reported an error: {}", + err.device_name, err.message + ), + }); + } + } +} + +#[derive(Default)] +struct LiveLoopState { + resampler: Option, + capture_buffer: Vec, + wav_writer: Option, + buffer_start_sample: u64, + dropped_audio_ms: u64, + chunk_id: u32, + inflight: Option, + resampler_flushed: bool, + result_listener_lost: bool, + recent_segments: Vec, +} + +impl LiveLoopState { + fn new(wav_writer: Option) -> Self { + Self { + wav_writer, + ..Self::default() + } + } +} + +struct LiveSessionRuntime { + session_id: u64, + engine: Arc, + config: StartLiveTranscriptionConfig, + audio_path: Option, + dictionary_terms: Vec, + result_channel: Channel, + status_channel: Channel, + stop_flag: Arc, + capture: ActiveCapture, + state: LiveLoopState, +} + +impl LiveSessionRuntime { + fn new( + session_id: u64, + engine: Arc, + config: StartLiveTranscriptionConfig, + audio_path: Option, + dictionary_terms: Vec, + result_channel: Channel, + status_channel: Channel, + stop_flag: Arc, + ) -> Result { + let capture = ActiveCapture::start(&config)?; + let wav_writer = open_wav_writer(&engine, audio_path.as_ref(), session_id, &status_channel); + Ok(Self { + session_id, + engine, + config, + audio_path, + dictionary_terms, + result_channel, + status_channel, + stop_flag, + capture, + state: LiveLoopState::new(wav_writer), + }) + } + + fn run(mut self) -> Result { + loop { + self.poll_inference()?; + self.capture + .drain_runtime_errors(self.session_id, &self.status_channel); + if let Some(chunk) = self.recv_audio()? { + self.process_audio_chunk(chunk)?; + } + self.drop_pending_overflow(); + self.flush_tail_if_stopping()?; + if self.dispatch_inference_if_ready() { + continue; + } + if self.should_exit_loop() { + break; + } + } + + self.drain_inference()?; + self.finish() + } + + fn poll_inference(&mut self) -> Result<(), String> { + let _ = poll_inference( + &mut self.state.inflight, + &mut self.state.result_listener_lost, + self.session_id, + &self.config, + &mut self.state.recent_segments, + &self.dictionary_terms, + &self.result_channel, + &self.status_channel, + )?; + Ok(()) + } + + fn recv_audio(&mut self) -> Result, String> { + match self.capture.rx.recv_timeout(Duration::from_millis(25)) { + Ok(chunk) => Ok(Some(chunk)), + Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Ok(None), + Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => { + let message = "Microphone capture disconnected unexpectedly".to_string(); + let _ = self.status_channel.send(LiveStatusMessage::Error { + session_id: self.session_id, + message: message.clone(), + }); + Err(message) + } + } + } + + fn process_audio_chunk(&mut self, chunk: AudioChunk) -> Result<(), String> { + let mono = downmix_chunk(chunk.samples, chunk.channels as usize); + let resampler = match &mut self.state.resampler { + Some(resampler) => resampler, + None => { + self.state.resampler = + Some(StreamingResampler::new(chunk.sample_rate).map_err(|e| e.to_string())?); + self.state.resampler.as_mut().expect("resampler just set") + } + }; + let resampled = resampler.push_samples(&mono).map_err(|e| e.to_string())?; + append_resampled_audio( + &mut self.state.capture_buffer, + &mut self.state.wav_writer, + &resampled, + self.session_id, + &self.status_channel, + ); + Ok(()) + } + + fn drop_pending_overflow(&mut self) { + if self.state.inflight.is_none() || self.state.capture_buffer.len() <= MAX_PENDING_SAMPLES { + return; + } + let overflow = self.state.capture_buffer.len() - MAX_PENDING_SAMPLES; + self.state.capture_buffer.drain(..overflow); + self.state.buffer_start_sample = self + .state + .buffer_start_sample + .saturating_add(overflow as u64); + self.state.dropped_audio_ms = self + .state + .dropped_audio_ms + .saturating_add((overflow as u64 * 1000) / WHISPER_SAMPLE_RATE as u64); + let _ = self.status_channel.send(LiveStatusMessage::Overload { + session_id: self.session_id, + dropped_audio_ms: self.state.dropped_audio_ms, + message: "Kon dropped older audio to keep live dictation responsive".into(), + }); + } + + fn flush_tail_if_stopping(&mut self) -> Result<(), String> { + if !self.stopping() || self.state.resampler_flushed { + return Ok(()); + } + if let Some(resampler) = &mut self.state.resampler { + let tail = resampler.flush().map_err(|e| e.to_string())?; + append_resampled_audio( + &mut self.state.capture_buffer, + &mut self.state.wav_writer, + &tail, + self.session_id, + &self.status_channel, + ); + } + self.flush_wav_header(); + self.state.resampler_flushed = true; + Ok(()) + } + + fn flush_wav_header(&mut self) { + let Some(writer) = self.state.wav_writer.as_mut() else { + return; + }; + if let Err(e) = writer.flush() { + let _ = self.status_channel.send(LiveStatusMessage::Warning { + session_id: self.session_id, + message: format!("WAV flush failed near session end: {e}"), + }); + } + } + + fn dispatch_inference_if_ready(&mut self) -> bool { + if self.state.inflight.is_some() { + return false; + } + let stopping = self.stopping(); + if let Some(task) = maybe_dispatch_chunk( + &self.engine, + &self.config, + &mut self.state.capture_buffer, + &mut self.state.buffer_start_sample, + &mut self.state.chunk_id, + stopping, + &self.status_channel, + self.session_id, + ) { + self.state.inflight = Some(task); + return true; + } + false + } + + fn stopping(&self) -> bool { + self.stop_flag.load(Ordering::Relaxed) + } + + fn should_exit_loop(&self) -> bool { + self.stopping() && self.state.resampler_flushed && self.state.inflight.is_none() + } + + fn drain_inference(&mut self) -> Result<(), String> { + while self.state.inflight.is_some() { + self.poll_inference()?; + thread::sleep(Duration::from_millis(10)); + } + Ok(()) + } + + fn finish(mut self) -> Result { + let audio_path = finalize_wav_writer( + self.state.wav_writer.take(), + self.audio_path.as_ref(), + self.session_id, + &self.status_channel, + ); + Ok(LiveSessionSummary { + session_id: self.session_id, + dropped_audio_ms: self.state.dropped_audio_ms, + audio_path, + }) + } +} + struct InferenceTask { chunk_id: u32, chunk_start_sample: u64, @@ -198,6 +488,7 @@ pub async fn start_live_transcription_session( result_channel: Channel, status_channel: Channel, ) -> Result { + let _lifecycle = live_state.lifecycle.lock().await; { let running = live_state.running.lock().unwrap(); if running.is_some() { @@ -227,11 +518,8 @@ pub async fn start_live_transcription_session( // `TranscriptionOptions` construction (see `maybe_dispatch_chunk`) picks // up profile fallback + vocabulary injection without further plumbing. let request_prompt = config.initial_prompt.clone().unwrap_or_default(); - config.initial_prompt = build_initial_prompt( - &request_prompt, - &profile.initial_prompt, - &profile_terms, - ); + config.initial_prompt = + build_initial_prompt(&request_prompt, &profile.initial_prompt, &profile_terms); let model_id = config .model_id @@ -258,7 +546,10 @@ pub async fn start_live_transcription_session( // for save_audio=true and silently dropping the recording would // surprise them worse. let audio_path = if config.save_audio { - Some(resolve_recording_path(&app, config.output_folder.as_deref())?) + Some(resolve_recording_path( + &app, + config.output_folder.as_deref(), + )?) } else { None }; @@ -299,6 +590,7 @@ pub async fn stop_live_transcription_session( live_state: tauri::State<'_, LiveTranscriptionState>, session_id: u64, ) -> Result { + let _lifecycle = live_state.lifecycle.lock().await; let running = live_state.running.lock().unwrap().take(); let Some(running) = running else { return Err("No live transcription session is running".into()); @@ -360,203 +652,54 @@ fn run_live_session( // lifetime to the session — when the function returns, the Drop // impl lifts it. Item #9 in docs/whisper-ecosystem/brief.md. let _power_guard = PowerAssertion::begin("kon live dictation session"); + LiveSessionRuntime::new( + session_id, + engine, + config, + audio_path, + dictionary_terms, + result_channel, + status_channel, + stop_flag, + )? + .run() +} - let (mut capture, rx) = match config.microphone_device.as_deref() { - Some(name) if !name.is_empty() => MicrophoneCapture::start_with_device(name), - _ => MicrophoneCapture::start(), - } - .map_err(|e| e.to_string())?; - // Drain runtime stream errors into the status channel so the user - // gets a toast when the device disconnects mid-recording instead of - // silently producing empty transcripts. The `_capture` binding keeps - // the cpal stream alive for the duration of the session. - let mic_error_rx = capture.take_error_rx(); - let _capture = capture; - - let mut resampler: Option = None; - let mut capture_buffer: Vec = Vec::new(); - - // Progressive WAV writer (brief item #19). Sample rate comes from - // the loaded backend's capabilities (#13 wiring) so a future - // non-16kHz backend records at its native rate without further - // plumbing. The writer flushes its header every ~500 ms, so the - // file on disk is a playable WAV even if the process is killed. +fn open_wav_writer( + engine: &Arc, + audio_path: Option<&PathBuf>, + session_id: u64, + status_channel: &Channel, +) -> Option { let sample_rate = engine .capabilities() .map(|c| c.sample_rate) .unwrap_or(WHISPER_SAMPLE_RATE); - let mut wav_writer: Option = match audio_path.as_ref() { - Some(path) => match WavWriter::create(path, sample_rate, 1) { - Ok(w) => Some(w), - Err(e) => { - let _ = status_channel.send(LiveStatusMessage::Warning { - session_id, - message: format!( - "Failed to open audio recording file ({}); transcription will continue without saving audio.", - e - ), - }); - None - } - }, - None => None, - }; - // `reported_audio_path` is decided at end-of-session based on - // whether the writer finalised successfully, not at open time. - // This way a writer that dies mid-session (append error clearing - // wav_writer, or finalise returning Err) does not leak a stale - // path back to the frontend that might point to a file whose - // header is out of sync with its data chunk. - - let mut buffer_start_sample: u64 = 0; - let mut dropped_audio_ms: u64 = 0; - let mut chunk_id: u32 = 0; - let mut inflight: Option = None; - let mut resampler_flushed = false; - let mut recent_segments: Vec = Vec::new(); - - loop { - if let Some(_done) = poll_inference( - &mut inflight, - session_id, - &config, - &mut recent_segments, - &dictionary_terms, - &result_channel, - &status_channel, - )? {} - - // Surface any cpal runtime errors as warnings. Non-fatal: a hard - // disconnect will also drop the audio sender and be caught by - // the `Disconnected` arm below. This lets the user see a toast - // even when cpal recovers without tearing the stream down. - if let Some(err_rx) = &mic_error_rx { - while let Ok(err) = err_rx.try_recv() { - let _ = status_channel.send(LiveStatusMessage::Warning { - session_id, - message: format!( - "Microphone '{}' reported an error: {}", - err.device_name, err.message - ), - }); - } - } - - match rx.recv_timeout(Duration::from_millis(25)) { - Ok(chunk) => { - let mono = downmix_chunk(chunk.samples, chunk.channels as usize); - let resampler = match &mut resampler { - Some(resampler) => resampler, - None => { - resampler = Some( - StreamingResampler::new(chunk.sample_rate) - .map_err(|e| e.to_string())?, - ); - resampler.as_mut().expect("resampler just set") - } - }; - - let resampled = resampler.push_samples(&mono).map_err(|e| e.to_string())?; - append_resampled_audio( - &mut capture_buffer, - &mut wav_writer, - &resampled, - session_id, - &status_channel, - ); - } - Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {} - Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => { - let message = "Microphone capture disconnected unexpectedly".to_string(); - let _ = status_channel.send(LiveStatusMessage::Error { - session_id, - message: message.clone(), - }); - return Err(message); - } - } - - if inflight.is_some() && capture_buffer.len() > MAX_PENDING_SAMPLES { - let overflow = capture_buffer.len() - MAX_PENDING_SAMPLES; - capture_buffer.drain(..overflow); - buffer_start_sample = buffer_start_sample.saturating_add(overflow as u64); - dropped_audio_ms = dropped_audio_ms - .saturating_add((overflow as u64 * 1000) / WHISPER_SAMPLE_RATE as u64); - let _ = status_channel.send(LiveStatusMessage::Overload { + let path = audio_path?; + match WavWriter::create(path, sample_rate, 1) { + Ok(writer) => Some(writer), + Err(e) => { + let _ = status_channel.send(LiveStatusMessage::Warning { session_id, - dropped_audio_ms, - message: "Kon dropped older audio to keep live dictation responsive".into(), + message: format!( + "Failed to open audio recording file ({}); transcription will continue without saving audio.", + e + ), }); - } - - let stopping = stop_flag.load(Ordering::Relaxed); - if stopping && !resampler_flushed { - if let Some(resampler) = &mut resampler { - let tail = resampler.flush().map_err(|e| e.to_string())?; - append_resampled_audio( - &mut capture_buffer, - &mut wav_writer, - &tail, - session_id, - &status_channel, - ); - } - resampler_flushed = true; - // Final flush for the WAV header so the last chunk's header - // update is on disk before we drop into the inference drain. - if let Some(writer) = wav_writer.as_mut() { - if let Err(e) = writer.flush() { - let _ = status_channel.send(LiveStatusMessage::Warning { - session_id, - message: format!("WAV flush failed near session end: {e}"), - }); - } - } - } - - if inflight.is_none() { - if let Some(task) = maybe_dispatch_chunk( - &engine, - &config, - &mut capture_buffer, - &mut buffer_start_sample, - &mut chunk_id, - stopping, - &status_channel, - session_id, - ) { - inflight = Some(task); - continue; - } - - if stopping && resampler_flushed { - break; - } + None } } +} - while inflight.is_some() { - poll_inference( - &mut inflight, - session_id, - &config, - &mut recent_segments, - &dictionary_terms, - &result_channel, - &status_channel, - )?; - thread::sleep(Duration::from_millis(10)); - } - - // Finalise the progressive WAV writer and decide whether to - // report a path to the frontend. Only a clean finalise produces a - // reported path: a writer that died mid-session (wav_writer was - // already None) or a finalise that itself errored both yield - // `None`, so `StopLiveTranscriptionResponse.audio_path` reflects - // "recording is known-good" rather than "recording was attempted". - let audio_path = match wav_writer.take() { +fn finalize_wav_writer( + wav_writer: Option, + audio_path: Option<&PathBuf>, + session_id: u64, + status_channel: &Channel, +) -> Option { + match wav_writer { Some(writer) => match writer.finalize() { - Ok(()) => audio_path.as_ref().map(|p| p.to_string_lossy().to_string()), + Ok(()) => audio_path.map(|path| path.to_string_lossy().to_string()), Err(e) => { let _ = status_channel.send(LiveStatusMessage::Warning { session_id, @@ -568,13 +711,7 @@ fn run_live_session( } }, None => None, - }; - - Ok(LiveSessionSummary { - session_id, - dropped_audio_ms, - audio_path, - }) + } } fn append_resampled_audio( @@ -719,6 +856,7 @@ fn maybe_dispatch_chunk( fn poll_inference( inflight: &mut Option, + result_listener_lost: &mut bool, session_id: u64, config: &StartLiveTranscriptionConfig, recent_segments: &mut Vec, @@ -761,22 +899,30 @@ fn poll_inference( ); let segment_count = segments.len(); let delivered_segments = segments.clone(); - - result_channel - .send(LiveResultMessage { - session_id, - chunk_id: task.chunk_id, - chunk_start_secs, - duration: task.duration_secs, - language: timed.transcript.language().to_string(), - inference_ms: timed.inference_ms, - segments, - raw_text, - }) - .map_err(|e| e.to_string())?; + let result_message = LiveResultMessage { + session_id, + chunk_id: task.chunk_id, + chunk_start_secs, + duration: task.duration_secs, + language: timed.transcript.language().to_string(), + inference_ms: timed.inference_ms, + segments, + raw_text, + }; + let delivered = emit_live_result( + result_channel, + status_channel, + result_listener_lost, + &result_message, + ); remember_recent_segments(recent_segments, &delivered_segments, chunk_start_secs); eprintln!( - "[live] session {session_id}: delivered chunk {} with {} segments in {}ms{}", + "[live] session {session_id}: {} chunk {} with {} segments in {}ms{}", + if delivered { + "delivered" + } else { + "processed without listener for" + }, task.chunk_id, segment_count, timed.inference_ms, @@ -813,6 +959,33 @@ fn poll_inference( } } +fn emit_live_result( + result_channel: &Channel, + status_channel: &Channel, + result_listener_lost: &mut bool, + result_message: &LiveResultMessage, +) -> bool { + if *result_listener_lost { + return false; + } + + match result_channel.send(result_message.clone()) { + Ok(()) => true, + Err(err) => { + *result_listener_lost = true; + eprintln!( + "[live] session {}: result listener unavailable on chunk {}: {}; continuing without live updates", + result_message.session_id, result_message.chunk_id, err + ); + let _ = status_channel.send(LiveStatusMessage::Warning { + session_id: result_message.session_id, + message: "Live preview disconnected; transcription will continue in the background until you stop the session.".into(), + }); + false + } + } +} + fn trim_overlap_segments(segments: &mut Vec, trim_before_secs: f64) { if trim_before_secs <= 0.0 { return; @@ -1150,6 +1323,82 @@ fn downmix_chunk(samples: Vec, channels: usize) -> Vec { #[cfg(test)] mod tests { use super::*; + use tauri::ipc::InvokeResponseBody; + + fn noop_status_channel() -> Channel { + Channel::new(|_| Ok(())) + } + + fn collecting_status_channel(payloads: Arc>>) -> Channel { + Channel::new(move |body| { + if let InvokeResponseBody::Json(json) = body { + payloads.lock().unwrap().push(json); + } + Ok(()) + }) + } + + fn dummy_running_session( + id: u64, + release_join: Option>, + ) -> RunningLiveSession { + let stop_flag = Arc::new(AtomicBool::new(false)); + let handle = tokio::spawn(async move { + if let Some(notify) = release_join { + notify.notified().await; + } + Ok(LiveSessionSummary { + session_id: id, + dropped_audio_ms: 0, + audio_path: None, + }) + }); + RunningLiveSession { + id, + stop_flag, + handle, + status_channel: noop_status_channel(), + } + } + + async fn test_begin_session_start( + live_state: Arc, + session_id: u64, + release_setup: Option>, + ) -> Result { + let _lifecycle = live_state.lifecycle.lock().await; + { + let running = live_state.running.lock().unwrap(); + if running.is_some() { + return Err("A live transcription session is already running".into()); + } + } + if let Some(notify) = release_setup { + notify.notified().await; + } + *live_state.running.lock().unwrap() = Some(dummy_running_session(session_id, None)); + Ok(session_id) + } + + async fn test_stop_session( + live_state: Arc, + session_id: u64, + ) -> Result { + let _lifecycle = live_state.lifecycle.lock().await; + let running = live_state.running.lock().unwrap().take(); + let Some(running) = running else { + return Err("No live transcription session is running".into()); + }; + if running.id != session_id { + *live_state.running.lock().unwrap() = Some(running); + return Err(format!("Session {session_id} is not active")); + } + running.stop_flag.store(true, Ordering::Relaxed); + running + .handle + .await + .map_err(|e| format!("Live session task failed: {e}"))? + } fn segment(start: f64, end: f64, text: &str) -> Segment { Segment { @@ -1256,4 +1505,157 @@ mod tests { assert_eq!(decision.speech_window_count, 3); assert_eq!(decision.max_consecutive_speech_windows, 3); } + + #[test] + fn result_listener_loss_is_warned_once_and_not_treated_as_inference_failure() { + let statuses = Arc::new(Mutex::new(Vec::new())); + let status_channel = collecting_status_channel(statuses.clone()); + let result_channel = Channel::new(|_| Err(tauri::Error::FailedToReceiveMessage)); + let config = StartLiveTranscriptionConfig { + engine: "whisper".into(), + model_id: None, + language: Some("en".into()), + initial_prompt: None, + save_audio: false, + output_folder: None, + remove_fillers: false, + british_english: false, + anti_hallucination: false, + format_mode: "Raw".into(), + microphone_device: None, + profile_id: None, + }; + let mut recent_segments = Vec::new(); + let mut result_listener_lost = false; + + let (tx1, rx1) = std::sync::mpsc::channel(); + tx1.send(Ok(kon_transcription::TimedTranscript { + transcript: kon_core::types::Transcript::new( + vec![segment(0.0, 0.8, "first chunk")], + "en".into(), + 0.8, + ), + inference_ms: 12, + })) + .unwrap(); + let mut inflight = Some(InferenceTask { + chunk_id: 1, + chunk_start_sample: 0, + trim_before_secs: 0.0, + duration_secs: 0.8, + rx: rx1, + }); + + let first = poll_inference( + &mut inflight, + &mut result_listener_lost, + 77, + &config, + &mut recent_segments, + &[], + &result_channel, + &status_channel, + ) + .unwrap(); + + assert_eq!(first, Some(true)); + assert!(result_listener_lost); + assert!(inflight.is_none()); + assert_eq!(recent_segments.len(), 1); + let warning_count_after_first = statuses.lock().unwrap().len(); + assert_eq!(warning_count_after_first, 1); + assert!( + statuses.lock().unwrap()[0].contains("Live preview disconnected"), + "expected a warning about background continuation after listener loss" + ); + + let (tx2, rx2) = std::sync::mpsc::channel(); + tx2.send(Ok(kon_transcription::TimedTranscript { + transcript: kon_core::types::Transcript::new( + vec![segment(0.0, 0.9, "second chunk")], + "en".into(), + 0.9, + ), + inference_ms: 14, + })) + .unwrap(); + inflight = Some(InferenceTask { + chunk_id: 2, + chunk_start_sample: 16_000, + trim_before_secs: 0.0, + duration_secs: 0.9, + rx: rx2, + }); + + let second = poll_inference( + &mut inflight, + &mut result_listener_lost, + 77, + &config, + &mut recent_segments, + &[], + &result_channel, + &status_channel, + ) + .unwrap(); + + assert_eq!(second, Some(true)); + assert!(inflight.is_none()); + assert_eq!(recent_segments.len(), 2); + assert_eq!( + statuses.lock().unwrap().len(), + warning_count_after_first, + "listener-loss warning should only be emitted once" + ); + } + + #[tokio::test] + async fn concurrent_starts_allow_only_one_session_to_claim_the_slot() { + let live_state = Arc::new(LiveTranscriptionState::default()); + let release_setup = Arc::new(tokio::sync::Notify::new()); + + let first = tokio::spawn(test_begin_session_start( + live_state.clone(), + 1, + Some(release_setup.clone()), + )); + tokio::time::sleep(Duration::from_millis(20)).await; + + let second = tokio::spawn(test_begin_session_start(live_state.clone(), 2, None)); + tokio::time::sleep(Duration::from_millis(20)).await; + assert!( + !second.is_finished(), + "second start should wait on the lifecycle lock" + ); + + release_setup.notify_one(); + + assert_eq!(first.await.unwrap().unwrap(), 1); + let err = second.await.unwrap().unwrap_err(); + assert_eq!(err, "A live transcription session is already running"); + } + + #[tokio::test] + async fn start_waits_for_stop_to_finish_joining_before_reusing_slot() { + let live_state = Arc::new(LiveTranscriptionState::default()); + let release_join = Arc::new(tokio::sync::Notify::new()); + *live_state.running.lock().unwrap() = + Some(dummy_running_session(7, Some(release_join.clone()))); + + let stop = tokio::spawn(test_stop_session(live_state.clone(), 7)); + tokio::time::sleep(Duration::from_millis(20)).await; + + let start = tokio::spawn(test_begin_session_start(live_state.clone(), 8, None)); + tokio::time::sleep(Duration::from_millis(20)).await; + assert!( + !start.is_finished(), + "new start should block until stop finishes joining the old worker" + ); + + release_join.notify_one(); + + let summary = stop.await.unwrap().unwrap(); + assert_eq!(summary.session_id, 7); + assert_eq!(start.await.unwrap().unwrap(), 8); + } } diff --git a/src-tauri/src/commands/models.rs b/src-tauri/src/commands/models.rs index a05553f..21d32d1 100644 --- a/src-tauri/src/commands/models.rs +++ b/src-tauri/src/commands/models.rs @@ -8,10 +8,10 @@ use kon_core::constants::WHISPER_SAMPLE_RATE; use kon_core::hardware::{self, CpuFeatures}; use kon_core::model_registry::{self, Engine, LanguageSupport, ModelEntry}; use kon_core::types::{AudioSamples, ModelId, TranscriptionOptions}; -use kon_transcription::model_manager; -use kon_transcription::{load_parakeet, LocalEngine, Transcriber}; #[cfg(feature = "whisper")] use kon_transcription::load_whisper; +use kon_transcription::model_manager; +use kon_transcription::{load_parakeet, LocalEngine, Transcriber}; /// Map legacy size strings to ModelId. fn whisper_model_id(size: &str) -> ModelId { @@ -73,9 +73,7 @@ fn model_capability( } } -pub fn load_model_from_disk( - model_id: &ModelId, -) -> Result, String> { +pub fn load_model_from_disk(model_id: &ModelId) -> Result, String> { let entry = model_registry::find_model(model_id).ok_or_else(|| format!("Unknown model: {model_id}"))?; @@ -205,8 +203,7 @@ pub fn prewarm_default_model(whisper_engine: Arc) { // latency instead of the ~4–5s cold-start documented in // ufal/whisper_streaming #96 and #135. Silence returns // empty segments — the *work* is the context allocation. - let silence = - AudioSamples::mono_16khz(vec![0.0_f32; WHISPER_SAMPLE_RATE as usize]); + let silence = AudioSamples::mono_16khz(vec![0.0_f32; WHISPER_SAMPLE_RATE as usize]); let options = TranscriptionOptions::default(); match whisper_engine.transcribe_sync(&silence, &options) { Ok(_) => eprintln!("[startup] Whisper warm-up inference complete"), @@ -379,11 +376,7 @@ fn supported_accelerators() -> Vec { } else { AcceleratorTarget::Other }; - compose_accelerators( - cfg!(feature = "whisper"), - vulkan_loader_available(), - target, - ) + compose_accelerators(cfg!(feature = "whisper"), vulkan_loader_available(), target) } /// Report which backend whisper.cpp was actually able to initialise @@ -405,8 +398,7 @@ pub fn detect_active_compute_device() -> ActiveComputeDevice { kind: "cpu".into(), label: "CPU (fallback)".into(), reason: Some( - "MoltenVK / Vulkan loader not found — install the Vulkan SDK runtime." - .into(), + "MoltenVK / Vulkan loader not found — install the Vulkan SDK runtime.".into(), ), }; } diff --git a/src-tauri/src/commands/paste.rs b/src-tauri/src/commands/paste.rs index 5703ae8..2f9a3e1 100644 --- a/src-tauri/src/commands/paste.rs +++ b/src-tauri/src/commands/paste.rs @@ -396,7 +396,11 @@ fn detect_focused_window_class_macos() -> Option { return None; } let name = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if name.is_empty() { None } else { Some(name) } + if name.is_empty() { + None + } else { + Some(name) + } } #[cfg(target_os = "windows")] @@ -421,7 +425,11 @@ fn detect_focused_window_class_windows() -> Option { return None; } let name = String::from_utf8_lossy(&output.stdout).trim().to_string(); - if name.is_empty() { None } else { Some(name) } + if name.is_empty() { + None + } else { + Some(name) + } } fn trigger_paste_keystroke() -> Result { @@ -473,7 +481,10 @@ fn trigger_undo_keystroke() -> Result { } #[cfg(target_os = "linux")] -fn linux_paste(xdg_session_type: Option<&str>, wayland_display_set: bool) -> Result { +fn linux_paste( + xdg_session_type: Option<&str>, + wayland_display_set: bool, +) -> Result { for tool in pick_linux_backend_order(xdg_session_type, wayland_display_set) { match run_linux_tool(tool) { Ok(()) => return Ok(tool.to_string()), diff --git a/src-tauri/src/commands/power.rs b/src-tauri/src/commands/power.rs index c209950..aa6d33a 100644 --- a/src-tauri/src/commands/power.rs +++ b/src-tauri/src/commands/power.rs @@ -21,7 +21,17 @@ //! may still decide to idle us. We log when that happens so the //! diagnostics bundle has a breadcrumb. +use std::collections::HashMap; use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Mutex, OnceLock}; + +#[derive(Debug, Clone)] +pub struct PowerAssertionSnapshot { + pub id: usize, + pub reason: &'static str, + pub backend: &'static str, + pub acquired: bool, +} /// Handle for a single power assertion. Dropping it releases the /// assertion. Holders are expected to keep it alive in a field for @@ -32,12 +42,30 @@ pub struct PowerAssertion { #[allow(dead_code)] id: usize, reason: &'static str, + backend: &'static str, + acquired: bool, #[cfg(target_os = "macos")] activity: Option, } static NEXT_ID: AtomicUsize = AtomicUsize::new(1); +fn assertion_registry() -> &'static Mutex> { + static REGISTRY: OnceLock>> = OnceLock::new(); + REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) +} + +pub fn active_assertions_snapshot() -> Vec { + let mut snapshots = assertion_registry() + .lock() + .unwrap() + .values() + .cloned() + .collect::>(); + snapshots.sort_by_key(|snapshot| snapshot.id); + snapshots +} + impl PowerAssertion { /// Begin a power assertion for the given reason. On macOS this /// pins beginActivityWithOptions; on Linux/Windows it logs only @@ -47,12 +75,16 @@ impl PowerAssertion { #[cfg(target_os = "macos")] let activity = objc_bridge::begin_activity(reason).ok(); + #[cfg(target_os = "macos")] + let backend = "macos"; + #[cfg(target_os = "macos")] + let acquired = activity.is_some(); #[cfg(target_os = "macos")] - if activity.is_none() { - eprintln!( - "[power] macOS App Nap guard could not begin activity for reason '{reason}'" - ); + if acquired { + eprintln!("[power] began macOS App Nap guard #{id} for reason '{reason}'"); + } else { + eprintln!("[power] macOS App Nap guard could not begin activity for reason '{reason}'"); } #[cfg(not(target_os = "macos"))] @@ -63,9 +95,26 @@ impl PowerAssertion { let _ = reason; } + #[cfg(not(target_os = "macos"))] + let backend = "noop"; + #[cfg(not(target_os = "macos"))] + let acquired = false; + + assertion_registry().lock().unwrap().insert( + id, + PowerAssertionSnapshot { + id, + reason, + backend, + acquired, + }, + ); + Self { id, reason, + backend, + acquired, #[cfg(target_os = "macos")] activity, } @@ -77,64 +126,83 @@ impl Drop for PowerAssertion { #[cfg(target_os = "macos")] if let Some(handle) = self.activity.take() { objc_bridge::end_activity(handle); + eprintln!( + "[power] ended macOS App Nap guard #{} for reason '{}'", + self.id, self.reason + ); } + assertion_registry().lock().unwrap().remove(&self.id); let _ = (self.reason, self.id); + let _ = (self.backend, self.acquired); } } #[cfg(target_os = "macos")] mod objc_bridge { - //! Placeholder for the NSProcessInfo App-Nap bridge. - //! - //! A proper implementation calls: - //! `NSProcessInfo *info = [NSProcessInfo processInfo];` - //! `id activity = [info beginActivityWithOptions: - //! (NSActivityUserInitiated | NSActivityLatencyCritical) - //! reason:reasonNSString];` - //! and retains the returned object until `end_activity`. - //! - //! This workstream ships the PowerAssertion RAII guard + wiring - //! so `commands/live.rs` and `commands/llm.rs` can adopt it today - //! (matters on macOS, no-op elsewhere). The actual `objc2` bridge - //! lands in a follow-up commit that can introduce `objc2` + - //! `objc2-foundation` without touching the rest of the workspace - //! in the same change. - //! - //! Until then, `begin_activity` returns Err; callers (`begin()`) - //! log a warning but keep running, so recording continues to work - //! as today — the gap is just the App-Nap protection, not the - //! recording itself. + use objc2::rc::Retained; + use objc2::runtime::ProtocolObject; + use objc2_foundation::{NSActivityOptions, NSObjectProtocol, NSProcessInfo, NSString}; pub struct ActivityHandle { - #[allow(dead_code)] - retained: *mut std::ffi::c_void, + activity: Retained>, } - // SAFETY: The pointer is opaque to Rust; Foundation manages its - // lifetime via retain/release. We never dereference it directly. unsafe impl Send for ActivityHandle {} - pub fn begin_activity(_reason: &str) -> Result { - Err("macOS App Nap bridge not yet wired — objc2 integration tracked for a follow-up".into()) + pub fn begin_activity(reason: &str) -> Result { + let process_info = NSProcessInfo::processInfo(); + let reason = NSString::from_str(reason); + let options = NSActivityOptions::UserInitiated | NSActivityOptions::LatencyCritical; + let activity = process_info.beginActivityWithOptions_reason(options, &reason); + Ok(ActivityHandle { activity }) } - pub fn end_activity(_handle: ActivityHandle) {} + pub fn end_activity(handle: ActivityHandle) { + let process_info = NSProcessInfo::processInfo(); + unsafe { + process_info.endActivity(&handle.activity); + } + } } #[cfg(test)] mod tests { use super::*; + use std::sync::{Mutex, MutexGuard, OnceLock}; + + fn power_test_guard() -> MutexGuard<'static, ()> { + static TEST_GUARD: OnceLock> = OnceLock::new(); + TEST_GUARD.get_or_init(|| Mutex::new(())).lock().unwrap() + } + + fn clear_assertion_registry() { + assertion_registry().lock().unwrap().clear(); + } #[test] fn power_assertion_is_a_no_op_drop() { + let _guard = power_test_guard(); + clear_assertion_registry(); let guard = PowerAssertion::begin("test-reason"); + let snapshots = active_assertions_snapshot(); + assert!(snapshots.iter().any(|snapshot| snapshot.id == guard.id)); drop(guard); + assert!(active_assertions_snapshot().is_empty()); } #[test] fn multiple_assertions_get_unique_ids() { + let _guard = power_test_guard(); + clear_assertion_registry(); let a = PowerAssertion::begin("a"); let b = PowerAssertion::begin("b"); assert_ne!(a.id, b.id); + let snapshots = active_assertions_snapshot(); + assert_eq!(snapshots.len(), 2); + assert_eq!(snapshots[0].reason, "a"); + assert_eq!(snapshots[1].reason, "b"); + drop(a); + drop(b); + assert!(active_assertions_snapshot().is_empty()); } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4c8eb5d..52c2064 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -74,8 +74,10 @@ async fn save_preferences( /// known crashes that the HANDOVER documents working around with a manual /// env-var prefix: /// -/// env GDK_BACKEND=x11 WINIT_UNIX_BACKEND=x11 \ -/// WEBKIT_DISABLE_DMABUF_RENDERER=1 npm run tauri dev +/// ```sh +/// env GDK_BACKEND=x11 WINIT_UNIX_BACKEND=x11 \ +/// WEBKIT_DISABLE_DMABUF_RENDERER=1 npm run tauri dev +/// ``` /// /// Detect the Wayland session at startup and apply the env vars before /// anything else loads, so users do not need to remember the prefix and