Replaces 22 production eprintln! sites with structured tracing events across 8 files. Closes Area B of the post-prognosis residuals plan (docs/superpowers/plans/2026-05-12-engine-slop-residuals.md). Files touched (22 sites): - crates/hotkey/src/linux.rs (2) — hotplug watcher degraded-mode warnings - crates/ai-formatting/src/pipeline.rs (1) — LLM cleanup fallback warning - src-tauri/src/commands/transcription.rs (1) — chunking dispatch info - src-tauri/src/commands/diagnostics.rs (1) — crashes-dir setup warning - src-tauri/src/commands/tasks.rs (1) — malformed feedback row warning - src-tauri/src/commands/power.rs (3) — App Nap acquire/release/fail - src-tauri/src/commands/models.rs (5) — Whisper warmup lifecycle - src-tauri/src/commands/live.rs (8) — session start, chunk dispatch, per-chunk delivery, inference errors, worker disconnects, listener loss, status-channel cascade Levels: error for unrecoverable failures (inference disconnect, panic, status cascade), warn for recoverable degradation (LLM fallback, malformed rows, App Nap fail, hotplug watcher fail), info for lifecycle (session start, chunk processed, App Nap acquire/release, warmup complete, chunking dispatch), debug for per-chunk noise (speech-gate skip, chunk dispatch). Two new dependencies and two new filter targets: - tracing = "0.1" added to crates/hotkey and crates/ai-formatting - Default EnvFilter in src-tauri/src/lib.rs::init_tracing extended with magnotia_hotkey=info,magnotia_ai_formatting=info so the new targets emit at the default level Out of scope (intentional, left as-is): - crates/mcp/src/main.rs — CLI binary, stderr is the log contract (module docstring) so the JSON-RPC stdout stream stays clean - crates/*/tests/*.rs and crates/core/examples/tuning_log_demo.rs — test/example diagnostic output relies on --nocapture stdio semantics Discovery during sweep (not fixed — separate follow-up): hotkey crate has 6 existing log:: calls (log::error/warn/info/debug) but the workspace builds tracing-subscriber without the tracing-log feature, so those events are currently silent. Worth a follow-up to either add the tracing-log bridge or migrate hotkey's existing log:: calls to tracing::. Verification: - cargo fmt --all - cargo check --workspace --all-targets — clean - cargo test --workspace — 330+ tests, zero failures - rg eprintln! src-tauri/src/commands/ crates/hotkey/src/ crates/ai-formatting/src/ — zero hits Pre-existing working-tree churn in crates/llm/, src/lib/pages/, src/lib/utils/saveMarkdown.ts and the untracked phase10a dogfood notes deliberately left unstaged per Jake's instruction. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
491 lines
22 KiB
Rust
491 lines
22 KiB
Rust
mod commands;
|
|
// System tray uses Tauri's `tray-icon` feature which is desktop-only.
|
|
// Android has no tray surface — drop the module entirely on that target.
|
|
#[cfg(not(target_os = "android"))]
|
|
mod tray;
|
|
|
|
use std::sync::Arc;
|
|
use std::sync::Once;
|
|
use std::time::Instant;
|
|
|
|
use sqlx::SqlitePool;
|
|
use tauri::Manager;
|
|
use tracing_subscriber::EnvFilter;
|
|
|
|
use magnotia_core::types::EngineName;
|
|
use magnotia_llm::LlmEngine;
|
|
use magnotia_storage::{database_path, get_setting, init as init_db, prune_error_log, set_setting};
|
|
|
|
/// How long to retain `error_log` rows. Pruned once on startup.
|
|
/// 90 days is long enough to triage "this happened a few weeks ago"
|
|
/// reports and short enough that the table stays kilobyte-scale.
|
|
const ERROR_LOG_RETENTION_DAYS: i64 = 90;
|
|
use magnotia_transcription::LocalEngine;
|
|
|
|
static TRACING_INIT: Once = Once::new();
|
|
|
|
/// Shared app state holding the transcription engines and database pool.
|
|
pub struct AppState {
|
|
pub whisper_engine: Arc<LocalEngine>,
|
|
pub parakeet_engine: Arc<LocalEngine>,
|
|
pub db: SqlitePool,
|
|
pub llm_engine: Arc<LlmEngine>,
|
|
}
|
|
|
|
/// Holds the preferences init script for injection into secondary windows.
|
|
pub struct PreferencesScript(pub String);
|
|
|
|
/// Build a JavaScript snippet that applies saved preferences to the DOM
|
|
/// before Svelte mounts, preventing any flash of unstyled content.
|
|
fn build_preferences_script(prefs_json: Option<String>) -> String {
|
|
let json = prefs_json.unwrap_or_default();
|
|
if json.is_empty() {
|
|
return String::new();
|
|
}
|
|
let js_value: serde_json::Value = match serde_json::from_str(&json) {
|
|
Ok(value) => value,
|
|
Err(_) => return String::new(),
|
|
};
|
|
let js_obj = serde_json::to_string(&js_value).unwrap_or_else(|_| "{}".to_string());
|
|
format!(
|
|
r#"(function() {{
|
|
try {{
|
|
var p = {js_obj};
|
|
var el = document.documentElement;
|
|
if (p.theme === 'system') {{
|
|
el.dataset.theme = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
}} else if (p.theme) {{
|
|
el.dataset.theme = p.theme;
|
|
}}
|
|
if (p.zone && p.zone !== 'default') el.dataset.zone = p.zone;
|
|
if (p.accessibility) {{
|
|
var a = p.accessibility;
|
|
var fonts = {{ lexend: "'Lexend', system-ui, sans-serif", atkinson: "'Atkinson Hyperlegible Next', system-ui, sans-serif", opendyslexic: "'OpenDyslexic', system-ui, sans-serif" }};
|
|
if (a.fontFamily) {{ el.style.setProperty('--font-family-body', fonts[a.fontFamily] || fonts.lexend); el.dataset.fontFamily = a.fontFamily; }}
|
|
if (a.fontSize) el.style.setProperty('--font-size-body', a.fontSize + 'px');
|
|
if (a.letterSpacing != null) el.style.setProperty('--letter-spacing-body', a.letterSpacing + 'em');
|
|
if (a.lineHeight) el.style.setProperty('--line-height-body', String(a.lineHeight));
|
|
if (a.transcriptSize) el.style.setProperty('--text-transcript', a.transcriptSize + 'px');
|
|
if (a.bionicReading) el.dataset.bionicReading = 'true';
|
|
if (a.reduceMotion === 'on' || (a.reduceMotion === 'system' && window.matchMedia('(prefers-reduced-motion: reduce)').matches)) el.dataset.reduceMotion = 'true';
|
|
}}
|
|
}} catch (e) {{ console.warn('Preferences injection failed:', e); }}
|
|
}})();"#
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::build_preferences_script;
|
|
|
|
#[test]
|
|
fn preferences_script_injects_object_without_redundant_json_parse() {
|
|
let script = build_preferences_script(Some(r#"{"theme":"dark"}"#.to_string()));
|
|
assert!(script.contains("var p = {\"theme\":\"dark\"};"));
|
|
assert!(!script.contains("JSON.parse"));
|
|
}
|
|
|
|
#[test]
|
|
fn preferences_script_rejects_malformed_json() {
|
|
assert!(build_preferences_script(Some("not json".to_string())).is_empty());
|
|
}
|
|
}
|
|
|
|
/// Save preferences JSON to the SQLite settings table.
|
|
#[tauri::command]
|
|
async fn save_preferences(
|
|
state: tauri::State<'_, AppState>,
|
|
preferences: String,
|
|
) -> Result<(), String> {
|
|
set_setting(&state.db, "magnotia_preferences", &preferences)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
/// On Linux Wayland sessions (KDE, GNOME) WebKitGTK + DMABUF rendering hits
|
|
/// known crashes that the dev launcher works around by setting rendering
|
|
/// env vars before WebKitGTK/WINIT initialise. In development, launch via:
|
|
///
|
|
/// ```sh
|
|
/// npm run dev:tauri
|
|
/// ```
|
|
///
|
|
/// Detect the Wayland session at startup and warn when the process was not
|
|
/// launched with the safe Linux rendering env vars. Rust 2024 marks runtime
|
|
/// environment mutation unsafe in multi-threaded programs, so the app must be
|
|
/// launched with these values by `run.sh`, the desktop file, or a package
|
|
/// wrapper rather than patching them here.
|
|
///
|
|
/// Inspired by Open-Whispr's similar Chromium self-relaunch (with
|
|
/// --ozone-platform=x11). Day 6 of the upgrade plan.
|
|
#[cfg(target_os = "linux")]
|
|
fn warn_if_x11_env_unset_on_wayland() {
|
|
let warn_if_unset = |key: &str, value: &str, why: &str| {
|
|
if std::env::var_os(key).is_none() {
|
|
tracing::warn!(
|
|
target: "magnotia_startup",
|
|
key,
|
|
expected = value,
|
|
why,
|
|
"Linux rendering workaround env var is not set; configure the launcher instead of mutating process env at runtime"
|
|
);
|
|
}
|
|
};
|
|
|
|
// Always-on Linux: disable WebKit's DMA-BUF renderer. On RADV / Renoir
|
|
// class iGPUs and a number of NVIDIA driver stacks, the DMA-BUF path
|
|
// burns ~10pp idle GPU and ~10pp idle CPU compared to the legacy
|
|
// renderer for no visible quality difference. Empirically (env-var
|
|
// matrix on Ryzen 5 4650U / Vega 6, May 2026) toggling this dropped
|
|
// magnotia idle CPU 12.30% → 2.80% of one core and idle GPU 17% → 10%.
|
|
// Applies to both X11 and Wayland sessions; users can opt back in by
|
|
// setting WEBKIT_DISABLE_DMABUF_RENDERER=0 explicitly.
|
|
warn_if_unset(
|
|
"WEBKIT_DISABLE_DMABUF_RENDERER",
|
|
"1",
|
|
"iGPU idle-cost workaround",
|
|
);
|
|
|
|
// If the user is on a Wayland session, also force WebKitGTK + GDK
|
|
// onto X11 via XWayland. Idempotent: if a value is already set, keep
|
|
// it (the user knows best). PipeWire screen-capture portal still
|
|
// works fine through XWayland; we only redirect the GUI rendering
|
|
// path.
|
|
let session_type = std::env::var("XDG_SESSION_TYPE").unwrap_or_default();
|
|
if session_type.eq_ignore_ascii_case("wayland") {
|
|
warn_if_unset("GDK_BACKEND", "x11", "Wayland XWayland fallback");
|
|
warn_if_unset("WINIT_UNIX_BACKEND", "x11", "Wayland XWayland fallback");
|
|
}
|
|
}
|
|
|
|
fn init_tracing() {
|
|
TRACING_INIT.call_once(|| {
|
|
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
|
EnvFilter::new(
|
|
"warn,magnotia=info,magnotia_lib=info,magnotia_audio=info,magnotia_startup=info,magnotia_hotkey=info,magnotia_ai_formatting=info",
|
|
)
|
|
});
|
|
|
|
let _ = tracing_subscriber::fmt()
|
|
.with_env_filter(env_filter)
|
|
.with_writer(std::io::stderr)
|
|
.try_init();
|
|
});
|
|
}
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run() {
|
|
init_tracing();
|
|
|
|
#[cfg(target_os = "linux")]
|
|
warn_if_x11_env_unset_on_wayland();
|
|
|
|
// Capture Rust panics to disk so the diagnostic-report bundler in
|
|
// Settings → About can attach them. Local only; nothing transmitted.
|
|
commands::diagnostics::install_panic_hook();
|
|
|
|
let builder = tauri::Builder::default()
|
|
.plugin(tauri_plugin_opener::init())
|
|
.plugin(tauri_plugin_dialog::init())
|
|
// Phase 6 nudges: OS-native notifications. The plugin exposes
|
|
// isPermissionGranted / requestPermission / sendNotification on
|
|
// the JS side; our deliver_nudge wrapper guards those calls
|
|
// against the nudge-bus suppression rules and restricts
|
|
// invocation to the main window via ensure_main_window.
|
|
.plugin(tauri_plugin_notification::init());
|
|
|
|
// Desktop-only plugins. Each is either unsupported on Android
|
|
// (global-shortcut, autostart) or structurally meaningless on a
|
|
// single-window mobile app (window-state). Gating here matches the
|
|
// Cargo.toml `cfg(not(target_os = "android"))` block that drops the
|
|
// crates entirely on Android targets.
|
|
#[cfg(not(target_os = "android"))]
|
|
let builder = builder
|
|
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
|
// Phase 5 rituals: autostart. The plugin registers JS-facing
|
|
// commands (isEnabled / enable / disable) that the Settings
|
|
// toggle and first-run prompt invoke directly — no bespoke
|
|
// Rust commands needed. `LaunchAgent` is the non-root macOS
|
|
// install path (per-user, no sudo).
|
|
.plugin(tauri_plugin_autostart::init(
|
|
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
|
|
None,
|
|
))
|
|
// Remember size + position of every window across app restarts.
|
|
// Without this, secondary windows (preview overlay, task float,
|
|
// transcript viewer) open at whatever spot the compositor picks,
|
|
// which feels random. State is persisted per-window-label to
|
|
// app-data/window-state.json.
|
|
.plugin(tauri_plugin_window_state::Builder::default().build());
|
|
|
|
builder
|
|
.setup(|app| {
|
|
// Initialise database and startup settings in one runtime entry.
|
|
let db_path = database_path();
|
|
let (db, init_script) = tauri::async_runtime::block_on(async {
|
|
let t0 = Instant::now();
|
|
let db = init_db(&db_path)
|
|
.await
|
|
.map_err(|e| Box::new(e) as Box<dyn std::error::Error>)?;
|
|
tracing::info!(target: "magnotia_startup", elapsed_ms = t0.elapsed().as_millis(), "DB init complete");
|
|
|
|
// Prune old `error_log` rows so the table doesn't grow unbounded
|
|
// across months of dogfooding. Best-effort — a prune failure is
|
|
// not worth blocking startup over.
|
|
let t_prune = Instant::now();
|
|
match prune_error_log(&db, ERROR_LOG_RETENTION_DAYS).await {
|
|
Ok(n) if n > 0 => tracing::info!(
|
|
target: "magnotia_startup",
|
|
rows_removed = n,
|
|
retention_days = ERROR_LOG_RETENTION_DAYS,
|
|
elapsed_ms = t_prune.elapsed().as_millis(),
|
|
"error log prune complete"
|
|
),
|
|
Ok(_) => {}
|
|
Err(e) => tracing::warn!(target: "magnotia_startup", error = %e, "error log prune failed"),
|
|
}
|
|
|
|
// Load saved preferences for webview injection.
|
|
let t1 = Instant::now();
|
|
let prefs_json = get_setting(&db, "magnotia_preferences").await.unwrap_or(None);
|
|
tracing::info!(target: "magnotia_startup", elapsed_ms = t1.elapsed().as_millis(), "preferences load complete");
|
|
let init_script = build_preferences_script(prefs_json);
|
|
|
|
Ok::<_, Box<dyn std::error::Error>>((db, init_script))
|
|
})?;
|
|
|
|
// Apply preferences to the main window (defined in tauri.conf.json)
|
|
if let Some(main_window) = app.get_webview_window("main") {
|
|
if !init_script.is_empty() {
|
|
// Tauri's WebviewWindow.eval() is the standard API for
|
|
// running JS in a webview — not the JS eval() function
|
|
let _ = main_window.eval(&init_script);
|
|
}
|
|
|
|
// Auto-grant microphone permission on Linux (WebKitGTK).
|
|
// WebKitGTK doesn't show a permission dialog — it just denies
|
|
// getUserMedia by default. We connect to the permission-request
|
|
// signal and grant audio capture requests automatically.
|
|
#[cfg(target_os = "linux")]
|
|
{
|
|
main_window
|
|
.with_webview(|webview| {
|
|
use webkit2gtk::glib::prelude::Cast;
|
|
use webkit2gtk::{
|
|
PermissionRequest, PermissionRequestExt, SettingsExt,
|
|
UserMediaPermissionRequest, UserMediaPermissionRequestExt,
|
|
WebViewExt,
|
|
};
|
|
|
|
let wv: webkit2gtk::WebView = webview.inner().clone();
|
|
|
|
// Enable media stream in WebKit settings
|
|
if let Some(settings) = WebViewExt::settings(&wv) {
|
|
settings.set_enable_media_stream(true);
|
|
settings.set_enable_media_capabilities(true);
|
|
}
|
|
|
|
tracing::warn!(
|
|
target: "magnotia_startup",
|
|
"Linux WebKitGTK microphone permission requests are auto-granted for audio-only capture; other permission classes remain denied"
|
|
);
|
|
|
|
// Auto-grant microphone capture only. Other WebKitGTK
|
|
// permission requests are denied so future surfaces do
|
|
// not inherit camera/geolocation/pointer-lock access.
|
|
WebViewExt::connect_permission_request(
|
|
&wv,
|
|
|_wv, request: &PermissionRequest| {
|
|
if let Ok(media) =
|
|
request.clone().downcast::<UserMediaPermissionRequest>()
|
|
{
|
|
if media.is_for_audio_device()
|
|
&& !media.is_for_video_device()
|
|
{
|
|
request.allow();
|
|
} else {
|
|
request.deny();
|
|
}
|
|
} else {
|
|
request.deny();
|
|
}
|
|
true
|
|
},
|
|
);
|
|
})
|
|
.unwrap_or_else(|e| {
|
|
// Non-fatal: WebKitGTK may already have media
|
|
// capture wired by some compositors, or the
|
|
// signal binding may fail on unusual builds.
|
|
// Falling back means getUserMedia() prompts (or
|
|
// silently denies) instead of auto-granting,
|
|
// which is degraded but recoverable.
|
|
tracing::warn!(
|
|
target: "magnotia_startup",
|
|
error = %e,
|
|
"failed to configure webview media permissions"
|
|
);
|
|
});
|
|
}
|
|
|
|
// Close-to-tray: hide the window instead of exiting so the
|
|
// tray icon stays as the canonical entry point. Desktop-only;
|
|
// mobile has no tray and "hide on close" maps to the
|
|
// platform's own background-app behaviour.
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
let win = main_window.clone();
|
|
main_window.on_window_event(move |event| {
|
|
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
|
|
api.prevent_close();
|
|
let _ = win.hide();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Store init script for secondary windows (float, viewer)
|
|
app.manage(PreferencesScript(init_script));
|
|
|
|
app.manage(commands::hotkey::HotkeyState::new());
|
|
app.manage(commands::live::LiveTranscriptionState::default());
|
|
app.manage(commands::tts::TtsState::new());
|
|
app.manage(commands::meeting::MeetingState::new());
|
|
|
|
app.manage(AppState {
|
|
whisper_engine: Arc::new(LocalEngine::new(EngineName::new("whisper"))),
|
|
parakeet_engine: Arc::new(LocalEngine::new(EngineName::new("parakeet"))),
|
|
db,
|
|
llm_engine: Arc::new(LlmEngine::new()),
|
|
});
|
|
|
|
// Runtime-warning banner: push CPU-feature + Vulkan-loader
|
|
// fallbacks to the frontend so Settings can render a one-line
|
|
// hint. No-ops on a fully-supported box.
|
|
crate::commands::models::emit_runtime_warnings(app.handle());
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
if let Err(e) = tray::setup(app) {
|
|
tracing::warn!(target: "magnotia_startup", error = %e, "failed to setup tray");
|
|
}
|
|
|
|
Ok(())
|
|
})
|
|
.invoke_handler(tauri::generate_handler![
|
|
// Preferences
|
|
save_preferences,
|
|
// Whisper model management
|
|
commands::models::download_model,
|
|
commands::models::list_models,
|
|
commands::models::load_model,
|
|
commands::models::prewarm_default_model_cmd,
|
|
commands::models::check_engine,
|
|
commands::models::get_runtime_capabilities,
|
|
// Local LLM management
|
|
commands::llm::recommend_llm_tier,
|
|
commands::llm::check_llm_model,
|
|
commands::llm::download_llm_model,
|
|
commands::llm::load_llm_model,
|
|
commands::llm::unload_llm_model,
|
|
commands::llm::delete_llm_model,
|
|
commands::llm::get_llm_status,
|
|
commands::llm::test_llm_model,
|
|
commands::llm::cleanup_transcript_text_cmd,
|
|
// Parakeet model management
|
|
commands::models::download_parakeet_model,
|
|
commands::models::check_parakeet_model,
|
|
commands::models::list_parakeet_models,
|
|
commands::models::load_parakeet_model,
|
|
commands::models::check_parakeet_engine,
|
|
// Transcription
|
|
commands::transcription::transcribe_file,
|
|
// Audio
|
|
commands::audio::list_audio_devices,
|
|
// Tasks (canonical SQLite-backed task CRUD)
|
|
commands::tasks::create_task_cmd,
|
|
commands::tasks::list_tasks_cmd,
|
|
commands::tasks::update_task_cmd,
|
|
commands::tasks::complete_task_cmd,
|
|
commands::tasks::delete_task_cmd,
|
|
commands::tasks::uncomplete_task_cmd,
|
|
commands::tasks::set_task_energy_cmd,
|
|
commands::tasks::decompose_and_store,
|
|
commands::tasks::extract_tasks_from_transcript_cmd,
|
|
commands::tasks::list_subtasks_cmd,
|
|
commands::tasks::complete_subtask_cmd,
|
|
commands::tasks::list_recent_completions_cmd,
|
|
// HITL feedback (Phase 2 roadmap)
|
|
commands::feedback::record_feedback,
|
|
// Read aloud (Phase 4 roadmap)
|
|
commands::tts::tts_speak,
|
|
commands::tts::tts_stop,
|
|
commands::tts::tts_list_voices,
|
|
// Rituals (Phase 5 roadmap)
|
|
commands::rituals::get_last_morning_triage,
|
|
commands::rituals::mark_morning_triage_shown,
|
|
// Nudges (Phase 6 roadmap)
|
|
commands::nudges::deliver_nudge,
|
|
// Implementation intentions (Phase 7 roadmap)
|
|
commands::intentions::list_implementation_rules,
|
|
commands::intentions::create_implementation_rule,
|
|
commands::intentions::set_implementation_rule_enabled,
|
|
commands::intentions::mark_implementation_rule_fired,
|
|
commands::intentions::delete_implementation_rule,
|
|
// Profiles + profile terms (canonical SQLite-backed profile CRUD) — Task 12
|
|
commands::profiles::list_profiles_cmd,
|
|
commands::profiles::create_profile_cmd,
|
|
commands::profiles::update_profile_cmd,
|
|
commands::profiles::delete_profile_cmd,
|
|
commands::profiles::list_profile_terms_cmd,
|
|
commands::profiles::add_profile_term_cmd,
|
|
commands::profiles::learn_profile_terms_from_edit_cmd,
|
|
commands::profiles::delete_profile_term_cmd,
|
|
// Transcripts (canonical SQLite-backed history) — Day 4
|
|
commands::transcripts::add_transcript,
|
|
commands::transcripts::list_transcripts,
|
|
commands::transcripts::get_transcript,
|
|
commands::transcripts::update_transcript,
|
|
commands::transcripts::update_transcript_meta_cmd,
|
|
commands::transcripts::delete_transcript,
|
|
commands::transcripts::search_transcripts,
|
|
// Diagnostics (panic + error capture, manual report bundle)
|
|
commands::diagnostics::log_frontend_error,
|
|
commands::diagnostics::list_recent_errors_command,
|
|
commands::diagnostics::list_crash_files,
|
|
commands::diagnostics::generate_diagnostic_report,
|
|
commands::diagnostics::save_diagnostic_report,
|
|
commands::diagnostics::get_os_info,
|
|
commands::live::start_live_transcription_session,
|
|
commands::live::stop_live_transcription_session,
|
|
// Windows
|
|
commands::windows::open_task_window,
|
|
commands::windows::open_viewer_window,
|
|
commands::windows::open_preview_window,
|
|
// Clipboard
|
|
commands::clipboard::copy_to_clipboard,
|
|
// Filesystem (Phase 9 save-dialog path)
|
|
commands::fs::write_text_file_cmd,
|
|
// LLM content tags (Phase 9)
|
|
commands::llm::extract_content_tags_cmd,
|
|
// Paste (auto-insert at cursor)
|
|
commands::paste::paste_text,
|
|
commands::paste::paste_text_replacing,
|
|
commands::paste::detect_paste_backends,
|
|
// Meeting auto-capture (process-list poll)
|
|
commands::meeting::detect_meeting_processes,
|
|
// Hardware
|
|
commands::hardware::probe_system,
|
|
commands::hardware::rank_models,
|
|
// Hotkey (evdev — Wayland-compatible)
|
|
commands::hotkey::is_wayland_session,
|
|
commands::hotkey::check_hotkey_access,
|
|
commands::hotkey::start_evdev_hotkey,
|
|
commands::hotkey::update_evdev_hotkey,
|
|
commands::hotkey::stop_evdev_hotkey,
|
|
// Updater
|
|
commands::update::check_for_update,
|
|
])
|
|
.run(tauri::generate_context!())
|
|
.expect("error while running Magnotia");
|
|
}
|