// Thin bridge to the Tauri desktop shell. In the browser these are inert (isTauri() === false), // so the SPA stays a single codebase. We use the injected `window.__TAURI__` global (the shell // sets `withGlobalTauri`) instead of the @tauri-apps npm packages, so the browser build needs // no Tauri dependencies. export const isTauri = (): boolean => typeof (globalThis as any).__TAURI__ !== "undefined"; // "macos" | "windows" | "linux" — injected by the shell (std::env::consts::OS) before the // SPA loads; userAgent fallback covers browser dev. The macOS overlay-titlebar layout (and // its traffic-light compensations) must NEVER apply on Windows, which keeps its native // title bar (alignment bug, caught on Windows 2026-07-21). export const platformOS = (): string => { const injected = (globalThis as any).__OCW_PLATFORM__; if (typeof injected === "string" && injected) return injected; return /mac/i.test(navigator.userAgent) ? "macos" : /win/i.test(navigator.userAgent) ? "windows" : "linux"; }; export type DictationStatus = { recording: boolean; model_installed: boolean; model_verified: boolean; test_passed: boolean; download_in_progress: boolean; model_name: string; model_bytes: number; supported: boolean; device_summary: string; compatibility_reason: string | null; }; export type DictationDownloadProgress = { downloaded_bytes: number; total_bytes: number; }; const invoke = async (cmd: string, args?: Record): Promise => { const tauri = (globalThis as any).__TAURI__; if (!tauri?.core?.invoke) return null; try { return (await tauri.core.invoke(cmd, args)) as T; } catch { return null; } }; const invokeStrict = async (cmd: string, args?: Record): Promise => { const tauri = (globalThis as any).__TAURI__; if (!tauri?.core?.invoke) throw new Error("This feature is available in the desktop app."); return (await tauri.core.invoke(cmd, args)) as T; }; /** Open the native macOS folder picker (Tauri only). Returns the chosen path, or null. */ export async function pickFolder(): Promise { const path = await invoke("pick_folder"); return typeof path === "string" && path ? path : null; } /** The folder picker that works EVERYWHERE: Tauri's native dialog in the desktop shell, else the * sidecar-opened OS dialog (the sidecar is local, so the browser GUI still gets a real picker — * owner report 2026-07-04: "Browse" was desktop-only and the browser had paste-a-path only). */ export async function chooseFolder(): Promise { if (isTauri()) return pickFolder(); const { pickFolderViaServer } = await import("./api"); return pickFolderViaServer(); } /** Open-at-login (macOS LaunchAgent). */ export const getAutostart = () => invoke("get_autostart"); export const setAutostart = (enabled: boolean) => invoke("set_autostart", { enabled }); /** Keep this system awake so scheduled tasks fire while idle (caffeinate on macOS, * SetThreadExecutionState on Windows). Persists across restarts. */ export const getKeepAwake = () => invoke("get_keep_awake"); export const setKeepAwake = (enabled: boolean) => invoke("set_keep_awake", { enabled }); /** Begin native window dragging from a custom title/header region. */ export const startWindowDrag = () => invoke("start_window_drag"); // Local dictation is native-only. The browser build deliberately keeps this unavailable rather // than silently sending microphone audio to a server. export const getDictationStatus = () => invoke("get_dictation_status"); /** Instantaneous mic loudness 0..1 while recording (0 otherwise) — drives the composer's * live waveform. Cheap; poll at ~10Hz. */ export const getDictationLevel = () => invoke("dictation_level"); export const startDictation = () => invokeStrict("start_dictation"); export const stopDictation = () => invokeStrict("stop_dictation"); export const cancelDictation = () => invokeStrict("cancel_dictation"); export const downloadDictationModel = () => invokeStrict("download_dictation_model"); export const cancelDictationModelDownload = () => invokeStrict("cancel_dictation_model_download"); export const verifyDictationModel = () => invokeStrict("verify_dictation_model"); export const markDictationTestPassed = () => invokeStrict("mark_dictation_test_passed"); export const deleteDictationModel = () => invokeStrict("delete_dictation_model"); export async function listenDictationDownloadProgress( handler: (progress: DictationDownloadProgress) => void, ): Promise<() => void> { const listen = (globalThis as any).__TAURI__?.event?.listen; if (!listen) return () => {}; return (await listen("dictation-download-progress", (event: { payload: DictationDownloadProgress }) => { handler(event.payload); })) as () => void; } // --- Auto-update (desktop only; browser builds see null / throw) ----------------- export type UpdateInfo = { version: string; notes: string }; /** Ask the shell whether a newer release exists (verified manifest; see lib.rs). * null = up to date, unreachable endpoint, or not the desktop app. */ export const checkForUpdate = () => invoke("check_for_update"); /** Pre-fetch + verify the update bytes in the background so the install is instant. * The shell caches them keyed by version; calling again for the same version is a no-op. * Rejects when there is no update or the download fails — callers fall back to the * download-on-install path. */ export const downloadUpdate = () => invokeStrict("download_update"); /** Drop the pre-fetched update bundle (freed on "Later" so a dismissed release * doesn't pin tens of MB for a weeks-long app run). */ export const clearPendingUpdate = () => invokeStrict("clear_pending_update"); /** Install the update (pre-fetched bytes when available, else download + verify now), * then relaunch. Resolves only on failure paths (success restarts the process on macOS; * Windows hands off to the installer). */ export const installUpdate = () => invokeStrict("install_update"); /** Best-effort open a URL in the user's browser. Uses the Tauri opener plugin if present, else * `window.open`. The caller should also render the raw URL so it stays copyable if both no-op * (the desktop webview has no opener plugin wired yet). */ export function openExternal(url: string): void { const opener = (globalThis as any).__TAURI__?.opener; if (opener?.openUrl) { opener.openUrl(url).catch(() => window.open(url, "_blank", "noopener,noreferrer")); return; } window.open(url, "_blank", "noopener,noreferrer"); }