From db4885d79a9700ad0997c4660f6f74277411ce53 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Wed, 1 Oct 2025 08:31:52 -0700 Subject: [PATCH 01/26] chore: add daily-driver worktree plan; add macOS auto pause/resume stub comment --- apps/whispering/src-tauri/src/lib.rs | 4 + ...1T120000-daily-driver-combined-branches.md | 154 ++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 docs/specs/20251001T120000-daily-driver-combined-branches.md diff --git a/apps/whispering/src-tauri/src/lib.rs b/apps/whispering/src-tauri/src/lib.rs index 07765f57ed..3eb08a9414 100644 --- a/apps/whispering/src-tauri/src/lib.rs +++ b/apps/whispering/src-tauri/src/lib.rs @@ -106,6 +106,10 @@ pub async fn run() { use enigo::{Direction, Enigo, Key, Keyboard, Settings}; use tauri_plugin_clipboard_manager::ClipboardExt; +// macOS auto pause/resume media stub: guarded by future settings flag. +// Implementation TBD; this branch intentionally adds no behavior change yet. +// We will add a Tauri command and UI toggle in follow-ups. + /// Writes text at the cursor position using the clipboard sandwich technique /// /// This method preserves the user's existing clipboard content by: diff --git a/docs/specs/20251001T120000-daily-driver-combined-branches.md b/docs/specs/20251001T120000-daily-driver-combined-branches.md new file mode 100644 index 0000000000..e636853b2b --- /dev/null +++ b/docs/specs/20251001T120000-daily-driver-combined-branches.md @@ -0,0 +1,154 @@ +### Goal + +Create a separate, runnable checkout of Whispering that automatically merges multiple branches together for day-to-day use without disturbing the main working repository and its active branches. Also introduce a new branch for a macOS auto pause/resume media feature. + +### Proposed Approach: Git worktree based "daily driver" branch + +Use `git worktree` to maintain a sibling checkout that tracks a dedicated merge branch (e.g., `dave/daily-driver`). This branch will regularly merge these sources: + +- `whispering-makefile` (adds `apps/whispering/Makefile`) +- `Dave-MakeSoundsNotImpactMediaControl` (or optionally `Sounds-not-Impact-Media-Debug` if preferred) +- `Dave-auto-pause-resume-media` (new feature branch we will create with an initial no-op change) + +This keeps your main repo free to stay on any branch while the worktree is the combined "daily driver" you run. + +Key reasons for worktree: +- Multiple checkouts from the same repo without extra clones +- No extra remotes; shares objects, saves disk +- Clean separation of working directory for running the combined build + +### High-level flow + +1) In your current repo, create a persistent merge branch `dave/daily-driver`. +2) Add a worktree at `../whispering-daily-driver` checked out to `dave/daily-driver`. +3) Provide an orchestration script that: + - fetches latest + - checks out `dave/daily-driver` + - merges in the designated branches in a fixed order + - resolves trivial conflicts using ours/theirs strategy where appropriate (prompt when non-trivial) + - runs `make dev` from `apps/whispering/` in the worktree + +You can continue normal work in your main repo. When you want to use the app with all three branches, run the orchestrator; it updates the worktree and starts the app. + +### Branch list and merge order + +Default order (later items take precedence if overlapping changes): +1. `whispering-makefile` +2. `Dave-MakeSoundsNotImpactMediaControl` (or `Sounds-not-Impact-Media-Debug` if you confirm) +3. `Dave-auto-pause-resume-media` (new feature branch with incremental changes going forward) + +This order minimizes churn by layering the operational Makefile first, then your media control behavior, then the new auto pause/resume logic. + +### Orchestrator outline + +We will add a script (kept in this repo under `scripts/`) that works on the parent directory to manage the worktree: + +```bash +#!/usr/bin/env bash +set -euo pipefail + +REPO_DIR="$(cd "$(dirname "$0")"/.. && pwd)" +WORKTREE_DIR="${REPO_DIR}/../whispering-daily-driver" +MERGE_BRANCH="dave/daily-driver" +BRANCHES=( + "whispering-makefile" + "Dave-MakeSoundsNotImpactMediaControl" # or Sounds-not-Impact-Media-Debug + "Dave-auto-pause-resume-media" +) + +cd "$REPO_DIR" + +# Ensure merge branch exists +if ! git rev-parse --verify "$MERGE_BRANCH" >/dev/null 2>&1; then + git checkout -b "$MERGE_BRANCH" origin/main || git checkout -b "$MERGE_BRANCH" main +fi + +# Ensure worktree exists +if [ ! -d "$WORKTREE_DIR/.git" ]; then + git worktree add "$WORKTREE_DIR" "$MERGE_BRANCH" +fi + +# Update and merge +git fetch --all --prune + +pushd "$WORKTREE_DIR" >/dev/null +git checkout "$MERGE_BRANCH" +git reset --hard origin/main || git reset --hard main + +for b in "${BRANCHES[@]}"; do + git fetch origin "$b" || true + if git rev-parse --verify "origin/$b" >/dev/null 2>&1; then + echo "Merging $b into $MERGE_BRANCH" + git merge --no-edit "origin/$b" || { + echo "Merge conflict encountered for $b. Resolve here, commit, then re-run." >&2 + exit 1 + } + else + echo "Warning: branch $b not found on origin; skipping" + fi +done + +# Launch dev +if [ -f apps/whispering/Makefile ]; then + (cd apps/whispering && make dev) +else + echo "apps/whispering/Makefile not found; ensure whispering-makefile branch is merged" + exit 1 +fi + +popd >/dev/null +``` + +Notes: +- We reset `dave/daily-driver` to `main` (or `origin/main`) before each merge sequence to ensure a clean base. If you prefer incremental fast-forwards, we can switch to periodic merges without reset. +- If merge conflicts occur, the script stops and asks you to resolve them in the worktree. After resolving and committing, re-run the script. + +### New feature: macOS auto pause/resume media + +Scope for the initial branch `Dave-auto-pause-resume-media`: +- Add a settings flag in the main settings UI: โ€œAuto pause/resume media on recordโ€. +- macOS only: on command sequence start, issue a system media pause; on end, issue media play. If no media session is active, do nothing. +- Start with a small, inconsequential change to establish the branch; implement the real behavior in follow-ups. + +Implementation direction (later PRs): +- For macOS, prefer a native approach via Tauri command or AppleScript bridge. Options include: + - AppleScript via `osascript` to send media key events + - Tauri plugin calling MediaRemote (if feasible) or using lower-level APIs +- Guard feature behind settings flag and platform detection. + +### Files and locations (proposed) + +- `scripts/daily-driver-merge-and-run.sh`: Orchestrator (committed to this repo). +- Worktree directory: `../whispering-daily-driver` relative to repo root. +- Merge target branch: `dave/daily-driver`. + +### Open confirmations needed + +- Confirm exact branch names: + - Keep `Dave-MakeSoundsNotImpactMediaControl` or use `Sounds-not-Impact-Media-Debug` instead? + - Confirm the `whispering-makefile` branch name. +- Confirm `make dev` is the intended target in `apps/whispering/Makefile`. +- Confirm the sibling path `../whispering-daily-driver` is acceptable. + +### TODOs + +1. Create `Dave-auto-pause-resume-media` branch with a trivial change. +2. Add `scripts/daily-driver-merge-and-run.sh` orchestrator. +3. Initialize `dave/daily-driver` merge branch and worktree at `../whispering-daily-driver`. +4. Wire branch list and merge order; handle fetch/reset semantics. +5. Validate merges with your designated branches; resolve any conflicts. +6. Run `make dev` in the worktree and confirm end-to-end behavior. +7. Implement real macOS pause/resume logic behind settings flag (follow-up). + +### Rollback + +To remove the worktree safely: + +```bash +git worktree remove ../whispering-daily-driver +git branch -D dave/daily-driver # only if you want to drop the branch +``` + +If you want a clone-based alternative (fully separate repo) instead of worktrees, we can swap to a script that clones into `../whispering-daily-driver`, adds origin remote, then merges branches similarly. Worktrees are lighter and share objects, so they are the recommended default. + + From 34f92fef94613e8eecee5c24f81b13c95e379428 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:33:07 -0700 Subject: [PATCH 02/26] First draft --- apps/whispering/src-tauri/src/lib.rs | 13 ++ apps/whispering/src-tauri/src/macos_media.rs | 125 ++++++++++++++++++ apps/whispering/src/lib/query/commands.ts | 25 +++- apps/whispering/src/lib/query/index.ts | 2 + apps/whispering/src/lib/query/media.ts | 53 ++++++++ apps/whispering/src/lib/settings/settings.ts | 7 + .../src/routes/(config)/settings/+page.svelte | 12 ++ ...20251001T120000-auto-pause-resume-media.md | 46 +++++++ 8 files changed, 279 insertions(+), 4 deletions(-) create mode 100644 apps/whispering/src-tauri/src/macos_media.rs create mode 100644 apps/whispering/src/lib/query/media.ts create mode 100644 docs/specs/20251001T120000-auto-pause-resume-media.md diff --git a/apps/whispering/src-tauri/src/lib.rs b/apps/whispering/src-tauri/src/lib.rs index 3eb08a9414..69c26b5ebd 100644 --- a/apps/whispering/src-tauri/src/lib.rs +++ b/apps/whispering/src-tauri/src/lib.rs @@ -17,6 +17,16 @@ use windows_path::fix_windows_path; pub mod graceful_shutdown; use graceful_shutdown::send_sigint; +#[cfg(target_os = "macos")] +pub mod macos_media; +#[cfg(target_os = "macos")] +use macos_media::{macos_pause_active_media, macos_resume_media}; + +#[cfg(not(target_os = "macos"))] +pub mod macos_media; +#[cfg(not(target_os = "macos"))] +use macos_media::{macos_pause_active_media, macos_resume_media}; + #[cfg_attr(mobile, tauri::mobile_entry_point)] #[tokio::main] pub async fn run() { @@ -80,6 +90,9 @@ pub async fn run() { // Whisper transcription transcribe_with_whisper_cpp, send_sigint, + // macOS media control + macos_pause_active_media, + macos_resume_media, ]); let app = builder diff --git a/apps/whispering/src-tauri/src/macos_media.rs b/apps/whispering/src-tauri/src/macos_media.rs new file mode 100644 index 0000000000..bf466a519c --- /dev/null +++ b/apps/whispering/src-tauri/src/macos_media.rs @@ -0,0 +1,125 @@ +use tauri::State; + +#[derive(serde::Serialize, serde::Deserialize)] +pub struct PausedPlayers { + pub players: Vec, +} + +#[tauri::command] +pub async fn macos_pause_active_media() -> Result { + #[cfg(target_os = "macos")] + { + // AppleScript to check state and pause Music and Spotify if playing + let script = r#" +set pausedPlayers to {} + +-- Apple Music +try + tell application "Music" + if it is running then + if player state is playing then + pause + set end of pausedPlayers to "Music" + end if + end if + end tell +end try + +-- Spotify +try + tell application "Spotify" + if it is running then + if player state is playing then + pause + set end of pausedPlayers to "Spotify" + end if + end if + end tell +end try + +return pausedPlayers as string +"#; + + match run_osascript(script).await { + Ok(output) => { + let players = parse_comma_list(&output); + Ok(PausedPlayers { players }) + } + Err(e) => Err(format!("Failed to pause media: {}", e)), + } + } + + #[cfg(not(target_os = "macos"))] + { + Ok(PausedPlayers { players: vec![] }) + } +} + +#[tauri::command] +pub async fn macos_resume_media(players: Vec) -> Result<(), String> { + #[cfg(target_os = "macos")] + { + // Build AppleScript dynamically based on players + let mut script = String::new(); + for p in players { + match p.as_str() { + "Music" => { + script.push_str( + "try\n tell application \"Music\"\n if it is running then play\n end tell\nend try\n", + ); + } + "Spotify" => { + script.push_str( + "try\n tell application \"Spotify\"\n if it is running then play\n end tell\nend try\n", + ); + } + _ => {} + } + } + + if script.is_empty() { + return Ok(()); + } + + run_osascript(&script).await.map(|_| ()) + } + + #[cfg(not(target_os = "macos"))] + { + Ok(()) + } +} + +#[cfg(target_os = "macos")] +async fn run_osascript(script: &str) -> Result { + use tokio::process::Command; + + let output = Command::new("osascript") + .arg("-e") + .arg(script) + .output() + .await + .map_err(|e| e.to_string())?; + + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); + Ok(stdout) + } else { + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + Err(stderr) + } +} + +fn parse_comma_list(s: &str) -> Vec { + let trimmed = s.trim(); + if trimmed.is_empty() { + return vec![]; + } + trimmed + .split(',') + .map(|p| p.trim().to_string()) + .filter(|p| !p.is_empty()) + .collect() +} + + diff --git a/apps/whispering/src/lib/query/commands.ts b/apps/whispering/src/lib/query/commands.ts index f8770923d0..b7a891f723 100644 --- a/apps/whispering/src/lib/query/commands.ts +++ b/apps/whispering/src/lib/query/commands.ts @@ -6,6 +6,7 @@ import { Err, Ok } from 'wellcrafted/result'; import { defineMutation } from './_client'; import { delivery } from './delivery'; import { recorder } from './recorder'; +import { rpc } from './'; import { notify } from './notify'; import { recordings } from './recordings'; import { sound } from './sound'; @@ -31,7 +32,10 @@ const startManualRecording = defineMutation({ description: 'Setting up your recording environment...', }); const { data: deviceAcquisitionOutcome, error: startRecordingError } = - await recorder.startRecording.execute({ toastId }); + (await (async () => { + await rpc.media.pauseIfEnabled.execute(undefined); + return await recorder.startRecording.execute({ toastId }); + })()); if (startRecordingError) { notify.error.execute({ id: toastId, ...startRecordingError }); @@ -103,7 +107,11 @@ const stopManualRecording = defineMutation({ description: 'Finalizing your audio capture...', }); const { data: blob, error: stopRecordingError } = - await recorder.stopRecording.execute({ toastId }); + (await (async () => { + const result = await recorder.stopRecording.execute({ toastId }); + await rpc.media.resumePaused.execute(undefined); + return result; + })()); if (stopRecordingError) { notify.error.execute({ id: toastId, ...stopRecordingError }); return Err(stopRecordingError); @@ -153,6 +161,7 @@ const startVadRecording = defineMutation({ title: '๐ŸŽ™๏ธ Starting voice activated capture', description: 'Your voice activated capture is starting...', }); + await rpc.media.pauseIfEnabled.execute(undefined); const { data: deviceAcquisitionOutcome, error: startActiveListeningError } = await vadRecorder.startActiveListening.execute({ onSpeechStart: () => { @@ -256,7 +265,11 @@ const stopVadRecording = defineMutation({ description: 'Finalizing your voice activated capture...', }); const { error: stopVadError } = - await vadRecorder.stopActiveListening.execute(undefined); + (await (async () => { + const result = await vadRecorder.stopActiveListening.execute(undefined); + await rpc.media.resumePaused.execute(undefined); + return result; + })()); if (stopVadError) { notify.error.execute({ id: toastId, ...stopVadError }); return Err(stopVadError); @@ -305,7 +318,11 @@ export const commands = { description: 'Cleaning up recording session...', }); const { data: cancelRecordingResult, error: cancelRecordingError } = - await recorder.cancelRecording.execute({ toastId }); + (await (async () => { + const result = await recorder.cancelRecording.execute({ toastId }); + await rpc.media.resumePaused.execute(undefined); + return result; + })()); if (cancelRecordingError) { notify.error.execute({ id: toastId, ...cancelRecordingError }); return Err(cancelRecordingError); diff --git a/apps/whispering/src/lib/query/index.ts b/apps/whispering/src/lib/query/index.ts index 980a8b17c8..a720c465d5 100644 --- a/apps/whispering/src/lib/query/index.ts +++ b/apps/whispering/src/lib/query/index.ts @@ -16,6 +16,7 @@ import { transformations } from './transformations'; import { transformer } from './transformer'; import { tray } from './tray'; import { vadRecorder } from './vad-recorder'; +import { media } from './media'; /** * Unified namespace for all query operations. @@ -39,4 +40,5 @@ export const rpc = { transformer, notify, delivery, + media, }; diff --git a/apps/whispering/src/lib/query/media.ts b/apps/whispering/src/lib/query/media.ts new file mode 100644 index 0000000000..5ed37bdc53 --- /dev/null +++ b/apps/whispering/src/lib/query/media.ts @@ -0,0 +1,53 @@ +import { defineMutation } from './_client'; +import { settings } from '$lib/stores/settings.svelte'; +import { IS_MACOS } from '$lib/constants/platform/is-macos'; +import { tryAsync, Ok, Err } from 'wellcrafted/result'; + +type PausedPlayers = { players: string[] }; + +let pausedPlayers: string[] = []; + +async function invoke(command: string, args?: Record) { + // Prefer dynamic import to avoid bundling on web + const { invoke } = await import('@tauri-apps/api/core'); + return tryAsync({ + try: async () => await invoke(command, args), + catch: (error) => Err({ name: 'TauriInvokeError', command, error } as const), + }); +} + +export const media = { + pauseIfEnabled: defineMutation({ + mutationKey: ['media', 'pauseIfEnabled'] as const, + resultMutationFn: async () => { + const enabled = settings.value['system.autoPauseMediaDuringRecording']; + if (!enabled || !IS_MACOS || !window.__TAURI_INTERNALS__) return Ok(undefined); + + const { data, error } = await invoke('macos_pause_active_media'); + if (error) { + console.warn('[media] pause failed', error); + return Ok(undefined); + } + pausedPlayers = data.players ?? []; + return Ok(undefined); + }, + }), + + resumePaused: defineMutation({ + mutationKey: ['media', 'resumePaused'] as const, + resultMutationFn: async () => { + if (!IS_MACOS || !window.__TAURI_INTERNALS__) return Ok(undefined); + if (pausedPlayers.length === 0) return Ok(undefined); + + const players = [...pausedPlayers]; + pausedPlayers = []; + const { error } = await invoke('macos_resume_media', { players }); + if (error) { + console.warn('[media] resume failed', error); + } + return Ok(undefined); + }, + }), +}; + + diff --git a/apps/whispering/src/lib/settings/settings.ts b/apps/whispering/src/lib/settings/settings.ts index b70f93658f..6db2dd1cf3 100644 --- a/apps/whispering/src/lib/settings/settings.ts +++ b/apps/whispering/src/lib/settings/settings.ts @@ -101,6 +101,13 @@ export const settingsSchema = z.object({ 'system.alwaysOnTop': z.enum(ALWAYS_ON_TOP_VALUES).default('Never'), + /** + * macOS-only: when enabled, Whispering will pause active media players + * (Apple Music, Spotify) at the start of a recording and resume them when + * the recording/transcription completes or is cancelled. + */ + 'system.autoPauseMediaDuringRecording': z.boolean().default(true), + 'database.recordingRetentionStrategy': z .enum(['keep-forever', 'limit-count']) .default('keep-forever'), diff --git a/apps/whispering/src/routes/(config)/settings/+page.svelte b/apps/whispering/src/routes/(config)/settings/+page.svelte index bd4f7aedbf..c398168846 100644 --- a/apps/whispering/src/routes/(config)/settings/+page.svelte +++ b/apps/whispering/src/routes/(config)/settings/+page.svelte @@ -7,6 +7,7 @@ import { Separator } from '@repo/ui/separator'; import { ALWAYS_ON_TOP_OPTIONS } from '$lib/constants/ui'; import { settings } from '$lib/stores/settings.svelte'; + import { IS_MACOS } from '$lib/constants/platform/is-macos'; @@ -108,5 +109,16 @@ } placeholder="Select a language" /> + + {#if IS_MACOS} + settings.value['system.autoPauseMediaDuringRecording'], + (v) => settings.updateKey('system.autoPauseMediaDuringRecording', v) + } + /> + {/if} {/if} diff --git a/docs/specs/20251001T120000-auto-pause-resume-media.md b/docs/specs/20251001T120000-auto-pause-resume-media.md new file mode 100644 index 0000000000..c933ed6717 --- /dev/null +++ b/docs/specs/20251001T120000-auto-pause-resume-media.md @@ -0,0 +1,46 @@ +Title: Auto pause/resume media on macOS during recording + +Problem +- When starting a Whispering recording via keyboard shortcut, music keeps playing. We want Whispering to auto-pause active media at start and auto-resume at the end, with a user setting (default on), macOS-only for v1. + +Scope +- Platform: macOS only +- Apps: Apple Music and Spotify (controlled via AppleScript) +- Behavior: Pause on start of manual/VAD recording; resume on stop/cancel (manual) and stop (VAD). Resume only the players that were previously playing. + +UX +- Settings (macOS desktop only): "Autoโ€‘pause media during recording". Default on. Hidden on nonโ€‘macOS. + +Implementation +- Settings: add `system.autoPauseMediaDuringRecording: boolean` (default true) +- Desktop check: gate by `window.__TAURI_INTERNALS__` and `IS_MACOS` +- Tauri (Rust): new commands + - `macos_pause_active_media` โ†’ detects Music/Spotify; pauses any that are currently playing; returns list of paused players + - `macos_resume_media(players: string[])` โ†’ resumes only provided players +- Frontend query module `media`: + - `pauseIfEnabled` executes pause command if setting enabled + macOS desktop; stores paused players + - `resumePaused` resumes previously paused players; clears state; safe to call when none +- Lifecycle wiring: + - Manual: pause before `recorder.startRecording`; resume after `processRecordingPipeline` completes; resume on cancel + - VAD: pause before `vadRecorder.startActiveListening`; resume on `stopVadRecording` +- Error handling: Best-effort; failures should not block recording or result delivery + +QA +- Music playing โ†’ start โ†’ pauses; stop โ†’ resumes +- Spotify playing โ†’ start โ†’ pauses; stop โ†’ resumes +- Both playing โ†’ both paused/resumed +- Neither playing โ†’ no-op +- Cancel mid-recording โ†’ resumes +- Setting off โ†’ no pause/resume +- Non-macOS โ†’ option hidden; no changes + +Future +- Consider browser players via media key events or Now Playing API +- Windows/Linux support via platform-specific integrations + +Review +- Added macOS-only setting and UI +- Implemented Tauri AppleScript bridge for Music/Spotify pause/play +- Wired into manual and VAD flows to pause on start and resume on stop/cancel + - Known limitations: only Music and Spotify supported; browser players are not handled; best-effort AppleScript calls may fail silently; no Windows/Linux support yet. + From 121bb1272f6bed6c445e05080633ceeeae170e4b Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:37:25 -0700 Subject: [PATCH 03/26] i --- apps/whispering/src-tauri/src/lib.rs | 6 ++--- apps/whispering/src-tauri/src/macos_media.rs | 1 - apps/whispering/src/lib/query/commands.ts | 25 ++++---------------- apps/whispering/src/lib/query/index.ts | 4 ++-- 4 files changed, 9 insertions(+), 27 deletions(-) diff --git a/apps/whispering/src-tauri/src/lib.rs b/apps/whispering/src-tauri/src/lib.rs index 69c26b5ebd..4863f208f5 100644 --- a/apps/whispering/src-tauri/src/lib.rs +++ b/apps/whispering/src-tauri/src/lib.rs @@ -90,9 +90,9 @@ pub async fn run() { // Whisper transcription transcribe_with_whisper_cpp, send_sigint, - // macOS media control - macos_pause_active_media, - macos_resume_media, + // macOS media control (temporarily disabled) + // macos_pause_active_media, + // macos_resume_media, ]); let app = builder diff --git a/apps/whispering/src-tauri/src/macos_media.rs b/apps/whispering/src-tauri/src/macos_media.rs index bf466a519c..66d220db05 100644 --- a/apps/whispering/src-tauri/src/macos_media.rs +++ b/apps/whispering/src-tauri/src/macos_media.rs @@ -1,4 +1,3 @@ -use tauri::State; #[derive(serde::Serialize, serde::Deserialize)] pub struct PausedPlayers { diff --git a/apps/whispering/src/lib/query/commands.ts b/apps/whispering/src/lib/query/commands.ts index b7a891f723..f8770923d0 100644 --- a/apps/whispering/src/lib/query/commands.ts +++ b/apps/whispering/src/lib/query/commands.ts @@ -6,7 +6,6 @@ import { Err, Ok } from 'wellcrafted/result'; import { defineMutation } from './_client'; import { delivery } from './delivery'; import { recorder } from './recorder'; -import { rpc } from './'; import { notify } from './notify'; import { recordings } from './recordings'; import { sound } from './sound'; @@ -32,10 +31,7 @@ const startManualRecording = defineMutation({ description: 'Setting up your recording environment...', }); const { data: deviceAcquisitionOutcome, error: startRecordingError } = - (await (async () => { - await rpc.media.pauseIfEnabled.execute(undefined); - return await recorder.startRecording.execute({ toastId }); - })()); + await recorder.startRecording.execute({ toastId }); if (startRecordingError) { notify.error.execute({ id: toastId, ...startRecordingError }); @@ -107,11 +103,7 @@ const stopManualRecording = defineMutation({ description: 'Finalizing your audio capture...', }); const { data: blob, error: stopRecordingError } = - (await (async () => { - const result = await recorder.stopRecording.execute({ toastId }); - await rpc.media.resumePaused.execute(undefined); - return result; - })()); + await recorder.stopRecording.execute({ toastId }); if (stopRecordingError) { notify.error.execute({ id: toastId, ...stopRecordingError }); return Err(stopRecordingError); @@ -161,7 +153,6 @@ const startVadRecording = defineMutation({ title: '๐ŸŽ™๏ธ Starting voice activated capture', description: 'Your voice activated capture is starting...', }); - await rpc.media.pauseIfEnabled.execute(undefined); const { data: deviceAcquisitionOutcome, error: startActiveListeningError } = await vadRecorder.startActiveListening.execute({ onSpeechStart: () => { @@ -265,11 +256,7 @@ const stopVadRecording = defineMutation({ description: 'Finalizing your voice activated capture...', }); const { error: stopVadError } = - (await (async () => { - const result = await vadRecorder.stopActiveListening.execute(undefined); - await rpc.media.resumePaused.execute(undefined); - return result; - })()); + await vadRecorder.stopActiveListening.execute(undefined); if (stopVadError) { notify.error.execute({ id: toastId, ...stopVadError }); return Err(stopVadError); @@ -318,11 +305,7 @@ export const commands = { description: 'Cleaning up recording session...', }); const { data: cancelRecordingResult, error: cancelRecordingError } = - (await (async () => { - const result = await recorder.cancelRecording.execute({ toastId }); - await rpc.media.resumePaused.execute(undefined); - return result; - })()); + await recorder.cancelRecording.execute({ toastId }); if (cancelRecordingError) { notify.error.execute({ id: toastId, ...cancelRecordingError }); return Err(cancelRecordingError); diff --git a/apps/whispering/src/lib/query/index.ts b/apps/whispering/src/lib/query/index.ts index a720c465d5..57d3f29e2a 100644 --- a/apps/whispering/src/lib/query/index.ts +++ b/apps/whispering/src/lib/query/index.ts @@ -16,7 +16,7 @@ import { transformations } from './transformations'; import { transformer } from './transformer'; import { tray } from './tray'; import { vadRecorder } from './vad-recorder'; -import { media } from './media'; +// import { media } from './media'; /** * Unified namespace for all query operations. @@ -40,5 +40,5 @@ export const rpc = { transformer, notify, delivery, - media, + // media, }; From d23222e3370c1f2dd24e7aa7b1b6f0ebdb6eb6f8 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:40:58 -0700 Subject: [PATCH 04/26] iter --- apps/whispering/src-tauri/src/lib.rs | 6 +++--- apps/whispering/src/lib/query/commands.ts | 7 ++++--- apps/whispering/src/lib/query/index.ts | 4 ++-- apps/whispering/src/lib/query/media.ts | 3 +++ 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/whispering/src-tauri/src/lib.rs b/apps/whispering/src-tauri/src/lib.rs index 4863f208f5..69c26b5ebd 100644 --- a/apps/whispering/src-tauri/src/lib.rs +++ b/apps/whispering/src-tauri/src/lib.rs @@ -90,9 +90,9 @@ pub async fn run() { // Whisper transcription transcribe_with_whisper_cpp, send_sigint, - // macOS media control (temporarily disabled) - // macos_pause_active_media, - // macos_resume_media, + // macOS media control + macos_pause_active_media, + macos_resume_media, ]); let app = builder diff --git a/apps/whispering/src/lib/query/commands.ts b/apps/whispering/src/lib/query/commands.ts index f8770923d0..5271f91059 100644 --- a/apps/whispering/src/lib/query/commands.ts +++ b/apps/whispering/src/lib/query/commands.ts @@ -13,7 +13,8 @@ import { transcription } from './transcription'; import { transformations } from './transformations'; import { transformer } from './transformer'; import { vadRecorder } from './vad-recorder'; -import { rpc } from './'; +import { analytics } from './analytics'; +import { media } from './media'; // Track manual recording start time for duration calculation let manualRecordingStartTime: number | null = null; @@ -123,7 +124,7 @@ const stopManualRecording = defineMutation({ duration = Date.now() - manualRecordingStartTime; manualRecordingStartTime = null; // Reset for next recording } - rpc.analytics.logEvent.execute({ + analytics.logEvent.execute({ type: 'manual_recording_completed', blob_size: blob.size, duration, @@ -172,7 +173,7 @@ const startVadRecording = defineMutation({ sound.playSoundIfEnabled.execute('vad-capture'); // Log VAD recording completion - rpc.analytics.logEvent.execute({ + analytics.logEvent.execute({ type: 'vad_recording_completed', blob_size: blob.size, // VAD doesn't track duration by default diff --git a/apps/whispering/src/lib/query/index.ts b/apps/whispering/src/lib/query/index.ts index 57d3f29e2a..a720c465d5 100644 --- a/apps/whispering/src/lib/query/index.ts +++ b/apps/whispering/src/lib/query/index.ts @@ -16,7 +16,7 @@ import { transformations } from './transformations'; import { transformer } from './transformer'; import { tray } from './tray'; import { vadRecorder } from './vad-recorder'; -// import { media } from './media'; +import { media } from './media'; /** * Unified namespace for all query operations. @@ -40,5 +40,5 @@ export const rpc = { transformer, notify, delivery, - // media, + media, }; diff --git a/apps/whispering/src/lib/query/media.ts b/apps/whispering/src/lib/query/media.ts index 5ed37bdc53..987758a4b3 100644 --- a/apps/whispering/src/lib/query/media.ts +++ b/apps/whispering/src/lib/query/media.ts @@ -23,11 +23,13 @@ export const media = { const enabled = settings.value['system.autoPauseMediaDuringRecording']; if (!enabled || !IS_MACOS || !window.__TAURI_INTERNALS__) return Ok(undefined); + console.info('[media] attempting to pause active media...'); const { data, error } = await invoke('macos_pause_active_media'); if (error) { console.warn('[media] pause failed', error); return Ok(undefined); } + console.info('[media] paused players:', data.players); pausedPlayers = data.players ?? []; return Ok(undefined); }, @@ -41,6 +43,7 @@ export const media = { const players = [...pausedPlayers]; pausedPlayers = []; + console.info('[media] resuming players:', players); const { error } = await invoke('macos_resume_media', { players }); if (error) { console.warn('[media] resume failed', error); From c13f3b7f1d89a204f7dc72e2e70492643b8269cf Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:46:29 -0700 Subject: [PATCH 05/26] iter --- apps/whispering/src/lib/query/commands.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/whispering/src/lib/query/commands.ts b/apps/whispering/src/lib/query/commands.ts index 5271f91059..f726fc9072 100644 --- a/apps/whispering/src/lib/query/commands.ts +++ b/apps/whispering/src/lib/query/commands.ts @@ -31,6 +31,9 @@ const startManualRecording = defineMutation({ title: '๐ŸŽ™๏ธ Preparing to record...', description: 'Setting up your recording environment...', }); + // Pause media before starting recording + await media.pauseIfEnabled.execute(undefined); + const { data: deviceAcquisitionOutcome, error: startRecordingError } = await recorder.startRecording.execute({ toastId }); @@ -105,6 +108,9 @@ const stopManualRecording = defineMutation({ }); const { data: blob, error: stopRecordingError } = await recorder.stopRecording.execute({ toastId }); + + // Resume media after stopping recording + await media.resumePaused.execute(undefined); if (stopRecordingError) { notify.error.execute({ id: toastId, ...stopRecordingError }); return Err(stopRecordingError); @@ -154,6 +160,9 @@ const startVadRecording = defineMutation({ title: '๐ŸŽ™๏ธ Starting voice activated capture', description: 'Your voice activated capture is starting...', }); + // Pause media before starting VAD + await media.pauseIfEnabled.execute(undefined); + const { data: deviceAcquisitionOutcome, error: startActiveListeningError } = await vadRecorder.startActiveListening.execute({ onSpeechStart: () => { @@ -258,6 +267,9 @@ const stopVadRecording = defineMutation({ }); const { error: stopVadError } = await vadRecorder.stopActiveListening.execute(undefined); + + // Resume media after stopping VAD + await media.resumePaused.execute(undefined); if (stopVadError) { notify.error.execute({ id: toastId, ...stopVadError }); return Err(stopVadError); @@ -307,6 +319,9 @@ export const commands = { }); const { data: cancelRecordingResult, error: cancelRecordingError } = await recorder.cancelRecording.execute({ toastId }); + + // Resume media after canceling recording + await media.resumePaused.execute(undefined); if (cancelRecordingError) { notify.error.execute({ id: toastId, ...cancelRecordingError }); return Err(cancelRecordingError); From 800c83c68181d9b39855b91f97d8e64b24330fdc Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:49:48 -0700 Subject: [PATCH 06/26] Update +page.svelte --- apps/whispering/src/routes/(config)/settings/+page.svelte | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/whispering/src/routes/(config)/settings/+page.svelte b/apps/whispering/src/routes/(config)/settings/+page.svelte index c398168846..e56998602b 100644 --- a/apps/whispering/src/routes/(config)/settings/+page.svelte +++ b/apps/whispering/src/routes/(config)/settings/+page.svelte @@ -114,6 +114,7 @@ settings.value['system.autoPauseMediaDuringRecording'], (v) => settings.updateKey('system.autoPauseMediaDuringRecording', v) From 15b5097d6652846d8bb790fdc435f140ad2cc750 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:07:59 -0700 Subject: [PATCH 07/26] Move settings --- apps/whispering/src/lib/query/media.ts | 2 +- apps/whispering/src/lib/settings/settings.ts | 2 +- .../src/routes/(config)/settings/+page.svelte | 11 ----------- .../routes/(config)/settings/sound/+page.svelte | 17 +++++++++++++++++ 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/apps/whispering/src/lib/query/media.ts b/apps/whispering/src/lib/query/media.ts index 987758a4b3..130f115fcc 100644 --- a/apps/whispering/src/lib/query/media.ts +++ b/apps/whispering/src/lib/query/media.ts @@ -20,7 +20,7 @@ export const media = { pauseIfEnabled: defineMutation({ mutationKey: ['media', 'pauseIfEnabled'] as const, resultMutationFn: async () => { - const enabled = settings.value['system.autoPauseMediaDuringRecording']; + const enabled = settings.value['sound.autoPauseMediaDuringRecording']; if (!enabled || !IS_MACOS || !window.__TAURI_INTERNALS__) return Ok(undefined); console.info('[media] attempting to pause active media...'); diff --git a/apps/whispering/src/lib/settings/settings.ts b/apps/whispering/src/lib/settings/settings.ts index 6db2dd1cf3..becb6a3032 100644 --- a/apps/whispering/src/lib/settings/settings.ts +++ b/apps/whispering/src/lib/settings/settings.ts @@ -106,7 +106,7 @@ export const settingsSchema = z.object({ * (Apple Music, Spotify) at the start of a recording and resume them when * the recording/transcription completes or is cancelled. */ - 'system.autoPauseMediaDuringRecording': z.boolean().default(true), + 'sound.autoPauseMediaDuringRecording': z.boolean().default(true), 'database.recordingRetentionStrategy': z .enum(['keep-forever', 'limit-count']) diff --git a/apps/whispering/src/routes/(config)/settings/+page.svelte b/apps/whispering/src/routes/(config)/settings/+page.svelte index e56998602b..f251693a86 100644 --- a/apps/whispering/src/routes/(config)/settings/+page.svelte +++ b/apps/whispering/src/routes/(config)/settings/+page.svelte @@ -110,16 +110,5 @@ placeholder="Select a language" /> - {#if IS_MACOS} - settings.value['system.autoPauseMediaDuringRecording'], - (v) => settings.updateKey('system.autoPauseMediaDuringRecording', v) - } - /> - {/if} {/if} diff --git a/apps/whispering/src/routes/(config)/settings/sound/+page.svelte b/apps/whispering/src/routes/(config)/settings/sound/+page.svelte index 722c325e83..e98743677d 100644 --- a/apps/whispering/src/routes/(config)/settings/sound/+page.svelte +++ b/apps/whispering/src/routes/(config)/settings/sound/+page.svelte @@ -2,6 +2,7 @@ import { LabeledSwitch } from '$lib/components/labeled'; import { Separator } from '@repo/ui/separator'; import { settings } from '$lib/stores/settings.svelte'; + import { IS_MACOS } from '$lib/constants/platform'; @@ -76,6 +77,22 @@ + {#if IS_MACOS} + + + settings.value['sound.autoPauseMediaDuringRecording'], + (v) => settings.updateKey('sound.autoPauseMediaDuringRecording', v) + } + /> + {/if} + + + Date: Wed, 1 Oct 2025 16:11:33 -0700 Subject: [PATCH 08/26] Update +page.svelte --- .../(config)/settings/sound/+page.svelte | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/apps/whispering/src/routes/(config)/settings/sound/+page.svelte b/apps/whispering/src/routes/(config)/settings/sound/+page.svelte index e98743677d..aaa4f6b815 100644 --- a/apps/whispering/src/routes/(config)/settings/sound/+page.svelte +++ b/apps/whispering/src/routes/(config)/settings/sound/+page.svelte @@ -77,22 +77,6 @@ - {#if IS_MACOS} - - - settings.value['sound.autoPauseMediaDuringRecording'], - (v) => settings.updateKey('sound.autoPauseMediaDuringRecording', v) - } - /> - {/if} - - - settings.updateKey('sound.playOn.transformationComplete', v) } /> + + {#if IS_MACOS} + + +
+

Media Control

+ settings.value['sound.autoPauseMediaDuringRecording'], + (v) => settings.updateKey('sound.autoPauseMediaDuringRecording', v) + } + /> +
+ {/if} From 3ee524aa8b9db579fe3f68fb594aaff4d48b5ef1 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Wed, 1 Oct 2025 16:13:38 -0700 Subject: [PATCH 09/26] Update +page.svelte --- .../(config)/settings/sound/+page.svelte | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/apps/whispering/src/routes/(config)/settings/sound/+page.svelte b/apps/whispering/src/routes/(config)/settings/sound/+page.svelte index aaa4f6b815..400209c4e6 100644 --- a/apps/whispering/src/routes/(config)/settings/sound/+page.svelte +++ b/apps/whispering/src/routes/(config)/settings/sound/+page.svelte @@ -98,17 +98,14 @@ {#if IS_MACOS} -
-

Media Control

- settings.value['sound.autoPauseMediaDuringRecording'], - (v) => settings.updateKey('sound.autoPauseMediaDuringRecording', v) - } - /> -
+ settings.value['sound.autoPauseMediaDuringRecording'], + (v) => settings.updateKey('sound.autoPauseMediaDuringRecording', v) + } + /> {/if} From 46459fb17f103a85a2791fee2260d0c54158e092 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Wed, 22 Oct 2025 15:34:19 -0700 Subject: [PATCH 10/26] Delete 20251001T120000-daily-driver-combined-branches.md --- ...1T120000-daily-driver-combined-branches.md | 154 ------------------ 1 file changed, 154 deletions(-) delete mode 100644 docs/specs/20251001T120000-daily-driver-combined-branches.md diff --git a/docs/specs/20251001T120000-daily-driver-combined-branches.md b/docs/specs/20251001T120000-daily-driver-combined-branches.md deleted file mode 100644 index e636853b2b..0000000000 --- a/docs/specs/20251001T120000-daily-driver-combined-branches.md +++ /dev/null @@ -1,154 +0,0 @@ -### Goal - -Create a separate, runnable checkout of Whispering that automatically merges multiple branches together for day-to-day use without disturbing the main working repository and its active branches. Also introduce a new branch for a macOS auto pause/resume media feature. - -### Proposed Approach: Git worktree based "daily driver" branch - -Use `git worktree` to maintain a sibling checkout that tracks a dedicated merge branch (e.g., `dave/daily-driver`). This branch will regularly merge these sources: - -- `whispering-makefile` (adds `apps/whispering/Makefile`) -- `Dave-MakeSoundsNotImpactMediaControl` (or optionally `Sounds-not-Impact-Media-Debug` if preferred) -- `Dave-auto-pause-resume-media` (new feature branch we will create with an initial no-op change) - -This keeps your main repo free to stay on any branch while the worktree is the combined "daily driver" you run. - -Key reasons for worktree: -- Multiple checkouts from the same repo without extra clones -- No extra remotes; shares objects, saves disk -- Clean separation of working directory for running the combined build - -### High-level flow - -1) In your current repo, create a persistent merge branch `dave/daily-driver`. -2) Add a worktree at `../whispering-daily-driver` checked out to `dave/daily-driver`. -3) Provide an orchestration script that: - - fetches latest - - checks out `dave/daily-driver` - - merges in the designated branches in a fixed order - - resolves trivial conflicts using ours/theirs strategy where appropriate (prompt when non-trivial) - - runs `make dev` from `apps/whispering/` in the worktree - -You can continue normal work in your main repo. When you want to use the app with all three branches, run the orchestrator; it updates the worktree and starts the app. - -### Branch list and merge order - -Default order (later items take precedence if overlapping changes): -1. `whispering-makefile` -2. `Dave-MakeSoundsNotImpactMediaControl` (or `Sounds-not-Impact-Media-Debug` if you confirm) -3. `Dave-auto-pause-resume-media` (new feature branch with incremental changes going forward) - -This order minimizes churn by layering the operational Makefile first, then your media control behavior, then the new auto pause/resume logic. - -### Orchestrator outline - -We will add a script (kept in this repo under `scripts/`) that works on the parent directory to manage the worktree: - -```bash -#!/usr/bin/env bash -set -euo pipefail - -REPO_DIR="$(cd "$(dirname "$0")"/.. && pwd)" -WORKTREE_DIR="${REPO_DIR}/../whispering-daily-driver" -MERGE_BRANCH="dave/daily-driver" -BRANCHES=( - "whispering-makefile" - "Dave-MakeSoundsNotImpactMediaControl" # or Sounds-not-Impact-Media-Debug - "Dave-auto-pause-resume-media" -) - -cd "$REPO_DIR" - -# Ensure merge branch exists -if ! git rev-parse --verify "$MERGE_BRANCH" >/dev/null 2>&1; then - git checkout -b "$MERGE_BRANCH" origin/main || git checkout -b "$MERGE_BRANCH" main -fi - -# Ensure worktree exists -if [ ! -d "$WORKTREE_DIR/.git" ]; then - git worktree add "$WORKTREE_DIR" "$MERGE_BRANCH" -fi - -# Update and merge -git fetch --all --prune - -pushd "$WORKTREE_DIR" >/dev/null -git checkout "$MERGE_BRANCH" -git reset --hard origin/main || git reset --hard main - -for b in "${BRANCHES[@]}"; do - git fetch origin "$b" || true - if git rev-parse --verify "origin/$b" >/dev/null 2>&1; then - echo "Merging $b into $MERGE_BRANCH" - git merge --no-edit "origin/$b" || { - echo "Merge conflict encountered for $b. Resolve here, commit, then re-run." >&2 - exit 1 - } - else - echo "Warning: branch $b not found on origin; skipping" - fi -done - -# Launch dev -if [ -f apps/whispering/Makefile ]; then - (cd apps/whispering && make dev) -else - echo "apps/whispering/Makefile not found; ensure whispering-makefile branch is merged" - exit 1 -fi - -popd >/dev/null -``` - -Notes: -- We reset `dave/daily-driver` to `main` (or `origin/main`) before each merge sequence to ensure a clean base. If you prefer incremental fast-forwards, we can switch to periodic merges without reset. -- If merge conflicts occur, the script stops and asks you to resolve them in the worktree. After resolving and committing, re-run the script. - -### New feature: macOS auto pause/resume media - -Scope for the initial branch `Dave-auto-pause-resume-media`: -- Add a settings flag in the main settings UI: โ€œAuto pause/resume media on recordโ€. -- macOS only: on command sequence start, issue a system media pause; on end, issue media play. If no media session is active, do nothing. -- Start with a small, inconsequential change to establish the branch; implement the real behavior in follow-ups. - -Implementation direction (later PRs): -- For macOS, prefer a native approach via Tauri command or AppleScript bridge. Options include: - - AppleScript via `osascript` to send media key events - - Tauri plugin calling MediaRemote (if feasible) or using lower-level APIs -- Guard feature behind settings flag and platform detection. - -### Files and locations (proposed) - -- `scripts/daily-driver-merge-and-run.sh`: Orchestrator (committed to this repo). -- Worktree directory: `../whispering-daily-driver` relative to repo root. -- Merge target branch: `dave/daily-driver`. - -### Open confirmations needed - -- Confirm exact branch names: - - Keep `Dave-MakeSoundsNotImpactMediaControl` or use `Sounds-not-Impact-Media-Debug` instead? - - Confirm the `whispering-makefile` branch name. -- Confirm `make dev` is the intended target in `apps/whispering/Makefile`. -- Confirm the sibling path `../whispering-daily-driver` is acceptable. - -### TODOs - -1. Create `Dave-auto-pause-resume-media` branch with a trivial change. -2. Add `scripts/daily-driver-merge-and-run.sh` orchestrator. -3. Initialize `dave/daily-driver` merge branch and worktree at `../whispering-daily-driver`. -4. Wire branch list and merge order; handle fetch/reset semantics. -5. Validate merges with your designated branches; resolve any conflicts. -6. Run `make dev` in the worktree and confirm end-to-end behavior. -7. Implement real macOS pause/resume logic behind settings flag (follow-up). - -### Rollback - -To remove the worktree safely: - -```bash -git worktree remove ../whispering-daily-driver -git branch -D dave/daily-driver # only if you want to drop the branch -``` - -If you want a clone-based alternative (fully separate repo) instead of worktrees, we can swap to a script that clones into `../whispering-daily-driver`, adds origin remote, then merges branches similarly. Worktrees are lighter and share objects, so they are the recommended default. - - From 8f511bba152a69383ec2d30e7ede526c149affa0 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Thu, 23 Oct 2025 09:37:30 -0700 Subject: [PATCH 11/26] Update commands.ts --- apps/whispering/src/lib/query/commands.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/whispering/src/lib/query/commands.ts b/apps/whispering/src/lib/query/commands.ts index f726fc9072..140bd1e5b0 100644 --- a/apps/whispering/src/lib/query/commands.ts +++ b/apps/whispering/src/lib/query/commands.ts @@ -13,7 +13,7 @@ import { transcription } from './transcription'; import { transformations } from './transformations'; import { transformer } from './transformer'; import { vadRecorder } from './vad-recorder'; -import { analytics } from './analytics'; +import { rpc } from './'; import { media } from './media'; // Track manual recording start time for duration calculation @@ -130,7 +130,7 @@ const stopManualRecording = defineMutation({ duration = Date.now() - manualRecordingStartTime; manualRecordingStartTime = null; // Reset for next recording } - analytics.logEvent.execute({ + rpc.analytics.logEvent.execute({ type: 'manual_recording_completed', blob_size: blob.size, duration, @@ -182,7 +182,7 @@ const startVadRecording = defineMutation({ sound.playSoundIfEnabled.execute('vad-capture'); // Log VAD recording completion - analytics.logEvent.execute({ + rpc.analytics.logEvent.execute({ type: 'vad_recording_completed', blob_size: blob.size, // VAD doesn't track duration by default From d2f3c3b7aaabd52058b004719314feda8fc40943 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Fri, 24 Oct 2025 17:58:58 -0700 Subject: [PATCH 12/26] Also pause Books app --- apps/whispering/src-tauri/src/macos_media.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/apps/whispering/src-tauri/src/macos_media.rs b/apps/whispering/src-tauri/src/macos_media.rs index 66d220db05..2cd40f4e61 100644 --- a/apps/whispering/src-tauri/src/macos_media.rs +++ b/apps/whispering/src-tauri/src/macos_media.rs @@ -8,7 +8,7 @@ pub struct PausedPlayers { pub async fn macos_pause_active_media() -> Result { #[cfg(target_os = "macos")] { - // AppleScript to check state and pause Music and Spotify if playing + // AppleScript to check state and pause Music, Spotify, and Books if playing let script = r#" set pausedPlayers to {} @@ -36,6 +36,18 @@ try end tell end try +-- Books +try + tell application "Books" + if it is running then + if player state is playing then + pause + set end of pausedPlayers to "Books" + end if + end if + end tell +end try + return pausedPlayers as string "#; @@ -72,6 +84,11 @@ pub async fn macos_resume_media(players: Vec) -> Result<(), String> { "try\n tell application \"Spotify\"\n if it is running then play\n end tell\nend try\n", ); } + "Books" => { + script.push_str( + "try\n tell application \"Books\"\n if it is running then play\n end tell\nend try\n", + ); + } _ => {} } } From a261b168ef8d786c4732583a4135b177f2235f0d Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Fri, 24 Oct 2025 18:05:34 -0700 Subject: [PATCH 13/26] Update macos_media.rs --- apps/whispering/src-tauri/src/macos_media.rs | 21 ++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/apps/whispering/src-tauri/src/macos_media.rs b/apps/whispering/src-tauri/src/macos_media.rs index 2cd40f4e61..57acf29173 100644 --- a/apps/whispering/src-tauri/src/macos_media.rs +++ b/apps/whispering/src-tauri/src/macos_media.rs @@ -36,14 +36,19 @@ try end tell end try --- Books +-- Books (uses UI scripting since it lacks player state API) try - tell application "Books" - if it is running then - if player state is playing then - pause - set end of pausedPlayers to "Books" - end if + tell application "System Events" + if exists process "Books" then + tell process "Books" + -- Check if "Pause" menu item exists (means it's playing) + if exists menu item "Pause" of menu "Controls" of menu bar 1 then + tell application "Books" to activate + delay 0.2 + keystroke space + set end of pausedPlayers to "Books" + end if + end tell end if end tell end try @@ -86,7 +91,7 @@ pub async fn macos_resume_media(players: Vec) -> Result<(), String> { } "Books" => { script.push_str( - "try\n tell application \"Books\"\n if it is running then play\n end tell\nend try\n", + "try\n tell application \"Books\" to activate\n delay 0.2\n tell application \"System Events\"\n tell process \"Books\"\n keystroke space\n end tell\n end tell\nend try\n", ); } _ => {} From 6e2c18139437c27e5180bec7022564e342ead036 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Fri, 24 Oct 2025 18:09:17 -0700 Subject: [PATCH 14/26] Update macos_media.rs --- apps/whispering/src-tauri/src/macos_media.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/whispering/src-tauri/src/macos_media.rs b/apps/whispering/src-tauri/src/macos_media.rs index 57acf29173..7bf4ca35ae 100644 --- a/apps/whispering/src-tauri/src/macos_media.rs +++ b/apps/whispering/src-tauri/src/macos_media.rs @@ -43,9 +43,7 @@ try tell process "Books" -- Check if "Pause" menu item exists (means it's playing) if exists menu item "Pause" of menu "Controls" of menu bar 1 then - tell application "Books" to activate - delay 0.2 - keystroke space + click menu item "Pause" of menu "Controls" of menu bar 1 set end of pausedPlayers to "Books" end if end tell @@ -91,7 +89,7 @@ pub async fn macos_resume_media(players: Vec) -> Result<(), String> { } "Books" => { script.push_str( - "try\n tell application \"Books\" to activate\n delay 0.2\n tell application \"System Events\"\n tell process \"Books\"\n keystroke space\n end tell\n end tell\nend try\n", + "try\n tell application \"System Events\"\n tell process \"Books\"\n click menu item \"Play\" of menu \"Controls\" of menu bar 1\n end tell\n end tell\nend try\n", ); } _ => {} From 555868470cc2f9677cd6ba6f4a8cb98988cdca87 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Thu, 30 Oct 2025 11:15:53 -0700 Subject: [PATCH 15/26] debug --- apps/whispering/src-tauri/src/macos_media.rs | 26 +++++++++++--------- apps/whispering/src/lib/query/commands.ts | 6 +++++ apps/whispering/src/lib/query/media.ts | 4 +++ apps/whispering/src/lib/query/recorder.ts | 3 +++ 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/apps/whispering/src-tauri/src/macos_media.rs b/apps/whispering/src-tauri/src/macos_media.rs index 7bf4ca35ae..efe9092278 100644 --- a/apps/whispering/src-tauri/src/macos_media.rs +++ b/apps/whispering/src-tauri/src/macos_media.rs @@ -38,17 +38,19 @@ end try -- Books (uses UI scripting since it lacks player state API) try - tell application "System Events" - if exists process "Books" then - tell process "Books" - -- Check if "Pause" menu item exists (means it's playing) - if exists menu item "Pause" of menu "Controls" of menu bar 1 then - click menu item "Pause" of menu "Controls" of menu bar 1 - set end of pausedPlayers to "Books" - end if - end tell - end if - end tell + with timeout of 1 seconds + tell application "System Events" + if exists process "Books" then + tell process "Books" + -- Check if "Pause" menu item exists (means it's playing) + if exists menu item "Pause" of menu "Controls" of menu bar 1 then + click menu item "Pause" of menu "Controls" of menu bar 1 + set end of pausedPlayers to "Books" + end if + end tell + end if + end tell + end timeout end try return pausedPlayers as string @@ -89,7 +91,7 @@ pub async fn macos_resume_media(players: Vec) -> Result<(), String> { } "Books" => { script.push_str( - "try\n tell application \"System Events\"\n tell process \"Books\"\n click menu item \"Play\" of menu \"Controls\" of menu bar 1\n end tell\n end tell\nend try\n", + "try\n with timeout of 1 seconds\n tell application \"System Events\"\n tell process \"Books\"\n click menu item \"Play\" of menu \"Controls\" of menu bar 1\n end tell\n end tell\n end timeout\nend try\n", ); } _ => {} diff --git a/apps/whispering/src/lib/query/commands.ts b/apps/whispering/src/lib/query/commands.ts index 140bd1e5b0..ffdad35998 100644 --- a/apps/whispering/src/lib/query/commands.ts +++ b/apps/whispering/src/lib/query/commands.ts @@ -23,7 +23,9 @@ let manualRecordingStartTime: number | null = null; const startManualRecording = defineMutation({ mutationKey: ['commands', 'startManualRecording'] as const, resultMutationFn: async () => { + console.time('[startup] switchRecordingMode(manual)'); await settings.switchRecordingMode('manual'); + console.timeEnd('[startup] switchRecordingMode(manual)'); const toastId = nanoid(); notify.loading.execute({ @@ -32,10 +34,14 @@ const startManualRecording = defineMutation({ description: 'Setting up your recording environment...', }); // Pause media before starting recording + console.time('[startup] media.pauseIfEnabled'); await media.pauseIfEnabled.execute(undefined); + console.timeEnd('[startup] media.pauseIfEnabled'); + console.time('[startup] recorder.startRecording'); const { data: deviceAcquisitionOutcome, error: startRecordingError } = await recorder.startRecording.execute({ toastId }); + console.timeEnd('[startup] recorder.startRecording'); if (startRecordingError) { notify.error.execute({ id: toastId, ...startRecordingError }); diff --git a/apps/whispering/src/lib/query/media.ts b/apps/whispering/src/lib/query/media.ts index 130f115fcc..c94eeb9ddf 100644 --- a/apps/whispering/src/lib/query/media.ts +++ b/apps/whispering/src/lib/query/media.ts @@ -24,7 +24,9 @@ export const media = { if (!enabled || !IS_MACOS || !window.__TAURI_INTERNALS__) return Ok(undefined); console.info('[media] attempting to pause active media...'); + console.time('[media] macos_pause_active_media invoke'); const { data, error } = await invoke('macos_pause_active_media'); + console.timeEnd('[media] macos_pause_active_media invoke'); if (error) { console.warn('[media] pause failed', error); return Ok(undefined); @@ -44,7 +46,9 @@ export const media = { const players = [...pausedPlayers]; pausedPlayers = []; console.info('[media] resuming players:', players); + console.time('[media] macos_resume_media invoke'); const { error } = await invoke('macos_resume_media', { players }); + console.timeEnd('[media] macos_resume_media invoke'); if (error) { console.warn('[media] resume failed', error); } diff --git a/apps/whispering/src/lib/query/recorder.ts b/apps/whispering/src/lib/query/recorder.ts index 49640098c6..fd4b784866 100644 --- a/apps/whispering/src/lib/query/recorder.ts +++ b/apps/whispering/src/lib/query/recorder.ts @@ -69,10 +69,12 @@ export const recorder = { }; // Resolve the output folder - use default if null + console.time('[startup] resolveOutputFolder'); const outputFolder = window.__TAURI_INTERNALS__ ? (settings.value['recording.cpal.outputFolder'] ?? (await getDefaultRecordingsFolder())) : ''; + console.timeEnd('[startup] resolveOutputFolder'); const paramsMap = { navigator: { @@ -106,6 +108,7 @@ export const recorder = { : settings.value['recording.method'] ]; + console.info('[startup] recorder method:', params.method); const { data: deviceAcquisitionOutcome, error: startRecordingError } = await recorderService().startRecording(params, { sendStatus: (options) => From 7ffc3ef58e37f7065fea1c589b097da9d4fe9d9b Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Thu, 30 Oct 2025 11:27:04 -0700 Subject: [PATCH 16/26] Update macos_media.rs --- apps/whispering/src-tauri/src/macos_media.rs | 33 +++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/apps/whispering/src-tauri/src/macos_media.rs b/apps/whispering/src-tauri/src/macos_media.rs index efe9092278..d5377cbc86 100644 --- a/apps/whispering/src-tauri/src/macos_media.rs +++ b/apps/whispering/src-tauri/src/macos_media.rs @@ -37,21 +37,24 @@ try end try -- Books (uses UI scripting since it lacks player state API) -try - with timeout of 1 seconds - tell application "System Events" - if exists process "Books" then - tell process "Books" - -- Check if "Pause" menu item exists (means it's playing) - if exists menu item "Pause" of menu "Controls" of menu bar 1 then - click menu item "Pause" of menu "Controls" of menu bar 1 - set end of pausedPlayers to "Books" - end if - end tell - end if - end tell - end timeout -end try +-- Fast-path: only check Books if nothing else was paused +if (count of pausedPlayers) is 0 then + try + with timeout of 1 seconds + tell application "System Events" + if exists process "Books" then + tell process "Books" + -- Check if "Pause" menu item exists (means it's playing) + if exists menu item "Pause" of menu "Controls" of menu bar 1 then + click menu item "Pause" of menu "Controls" of menu bar 1 + set end of pausedPlayers to "Books" + end if + end tell + end if + end tell + end timeout + end try +end if return pausedPlayers as string "#; From 9a99ea10bc4af79710a3d72fb49ac007ce170894 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:39:00 -0700 Subject: [PATCH 17/26] debug --- apps/whispering/src-tauri/src/macos_media.rs | 105 ++++++++++++------- 1 file changed, 69 insertions(+), 36 deletions(-) diff --git a/apps/whispering/src-tauri/src/macos_media.rs b/apps/whispering/src-tauri/src/macos_media.rs index d5377cbc86..d1aa1d9ea1 100644 --- a/apps/whispering/src-tauri/src/macos_media.rs +++ b/apps/whispering/src-tauri/src/macos_media.rs @@ -8,64 +8,97 @@ pub struct PausedPlayers { pub async fn macos_pause_active_media() -> Result { #[cfg(target_os = "macos")] { - // AppleScript to check state and pause Music, Spotify, and Books if playing - let script = r#" -set pausedPlayers to {} - --- Apple Music + use std::time::Instant; + let start = Instant::now(); + + // Check Music first + let music_result = run_osascript(r#" try tell application "Music" if it is running then if player state is playing then pause - set end of pausedPlayers to "Music" + return "Music" end if end if end tell end try - --- Spotify +return "" +"#).await; + + let music_time = start.elapsed(); + eprintln!("[macos_media] Music check took {:?}", music_time); + + let mut paused_players = Vec::new(); + if let Ok(output) = music_result { + if !output.trim().is_empty() { + paused_players.push(output.trim().to_string()); + } + } + + // Check Spotify + let spotify_start = Instant::now(); + let spotify_result = run_osascript(r#" try tell application "Spotify" if it is running then if player state is playing then pause - set end of pausedPlayers to "Spotify" + return "Spotify" end if end if end tell end try - --- Books (uses UI scripting since it lacks player state API) --- Fast-path: only check Books if nothing else was paused -if (count of pausedPlayers) is 0 then - try - with timeout of 1 seconds +return "" +"#).await; + + let spotify_time = spotify_start.elapsed(); + eprintln!("[macos_media] Spotify check took {:?}", spotify_time); + + if let Ok(output) = spotify_result { + if !output.trim().is_empty() { + paused_players.push(output.trim().to_string()); + } + } + + // Only check Books if nothing else was paused + if paused_players.is_empty() { + let books_start = Instant::now(); + let books_result = run_osascript(r#" +try + with timeout of 0.3 seconds + tell application "System Events" + set booksProcessExists to exists process "Books" + end tell + if booksProcessExists then tell application "System Events" - if exists process "Books" then - tell process "Books" - -- Check if "Pause" menu item exists (means it's playing) - if exists menu item "Pause" of menu "Controls" of menu bar 1 then - click menu item "Pause" of menu "Controls" of menu bar 1 - set end of pausedPlayers to "Books" - end if - end tell - end if + tell process "Books" + if exists menu item "Pause" of menu "Controls" of menu bar 1 then + click menu item "Pause" of menu "Controls" of menu bar 1 + return "Books" + end if + end tell end tell - end timeout - end try -end if - -return pausedPlayers as string -"#; - - match run_osascript(script).await { - Ok(output) => { - let players = parse_comma_list(&output); - Ok(PausedPlayers { players }) + end if + end timeout +end try +return "" +"#).await; + + let books_time = books_start.elapsed(); + eprintln!("[macos_media] Books check took {:?}", books_time); + + if let Ok(output) = books_result { + if !output.trim().is_empty() { + paused_players.push(output.trim().to_string()); + } } - Err(e) => Err(format!("Failed to pause media: {}", e)), } + + let total_time = start.elapsed(); + eprintln!("[macos_media] Total pause took {:?}", total_time); + + return Ok(PausedPlayers { players: paused_players }); } #[cfg(not(target_os = "macos"))] From faa9f882ea89a294d8a605850142ceee126d061b Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:42:32 -0700 Subject: [PATCH 18/26] Update macos_media.rs --- apps/whispering/src-tauri/src/macos_media.rs | 87 +++++++++++--------- 1 file changed, 48 insertions(+), 39 deletions(-) diff --git a/apps/whispering/src-tauri/src/macos_media.rs b/apps/whispering/src-tauri/src/macos_media.rs index d1aa1d9ea1..6f5e455335 100644 --- a/apps/whispering/src-tauri/src/macos_media.rs +++ b/apps/whispering/src-tauri/src/macos_media.rs @@ -11,54 +11,63 @@ pub async fn macos_pause_active_media() -> Result { use std::time::Instant; let start = Instant::now(); - // Check Music first - let music_result = run_osascript(r#" + // Run Music and Spotify checks concurrently with short AppleScript timeouts + let music_script = r#" try - tell application "Music" - if it is running then - if player state is playing then - pause - return "Music" + with timeout of 0.5 seconds + tell application "Music" + if it is running then + if player state is playing then + pause + return "Music" + end if end if - end if - end tell + end tell + end timeout end try return "" -"#).await; - - let music_time = start.elapsed(); - eprintln!("[macos_media] Music check took {:?}", music_time); - - let mut paused_players = Vec::new(); - if let Ok(output) = music_result { - if !output.trim().is_empty() { - paused_players.push(output.trim().to_string()); - } - } - - // Check Spotify - let spotify_start = Instant::now(); - let spotify_result = run_osascript(r#" +"#; + + let spotify_script = r#" try - tell application "Spotify" - if it is running then - if player state is playing then - pause - return "Spotify" + with timeout of 0.5 seconds + tell application "Spotify" + if it is running then + if player state is playing then + pause + return "Spotify" + end if end if - end if - end tell + end tell + end timeout end try return "" -"#).await; - - let spotify_time = spotify_start.elapsed(); - eprintln!("[macos_media] Spotify check took {:?}", spotify_time); - - if let Ok(output) = spotify_result { - if !output.trim().is_empty() { - paused_players.push(output.trim().to_string()); +"#; + + let music_start = Instant::now(); + let spotify_start = Instant::now(); + let (music_out, spotify_out) = tokio::join!( + async { + let r = run_osascript(music_script).await; + let d = music_start.elapsed(); + (r, d) + }, + async { + let r = run_osascript(spotify_script).await; + let d = spotify_start.elapsed(); + (r, d) } + ); + + eprintln!("[macos_media] Music check took {:?}", music_out.1); + eprintln!("[macos_media] Spotify check took {:?}", spotify_out.1); + + let mut paused_players = Vec::new(); + if let Ok(output) = music_out.0 { + if !output.trim().is_empty() { paused_players.push(output.trim().to_string()); } + } + if let Ok(output) = spotify_out.0 { + if !output.trim().is_empty() { paused_players.push(output.trim().to_string()); } } // Only check Books if nothing else was paused From 0a6a1e237787aa61bd36ccbb235c9440b67e4d50 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:55:41 -0700 Subject: [PATCH 19/26] Attempt async --- apps/whispering/src-tauri/src/macos_media.rs | 4 +-- apps/whispering/src/lib/query/commands.ts | 29 ++++++++++++++------ apps/whispering/src/lib/query/media.ts | 19 +++++++------ 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/apps/whispering/src-tauri/src/macos_media.rs b/apps/whispering/src-tauri/src/macos_media.rs index 6f5e455335..dee4d769a4 100644 --- a/apps/whispering/src-tauri/src/macos_media.rs +++ b/apps/whispering/src-tauri/src/macos_media.rs @@ -14,7 +14,7 @@ pub async fn macos_pause_active_media() -> Result { // Run Music and Spotify checks concurrently with short AppleScript timeouts let music_script = r#" try - with timeout of 0.5 seconds + with timeout of 0.2 seconds tell application "Music" if it is running then if player state is playing then @@ -30,7 +30,7 @@ return "" let spotify_script = r#" try - with timeout of 0.5 seconds + with timeout of 0.2 seconds tell application "Spotify" if it is running then if player state is playing then diff --git a/apps/whispering/src/lib/query/commands.ts b/apps/whispering/src/lib/query/commands.ts index ffdad35998..311c94a530 100644 --- a/apps/whispering/src/lib/query/commands.ts +++ b/apps/whispering/src/lib/query/commands.ts @@ -18,6 +18,7 @@ import { media } from './media'; // Track manual recording start time for duration calculation let manualRecordingStartTime: number | null = null; +let currentMediaSessionId: string | null = null; // Internal mutations for manual recording const startManualRecording = defineMutation({ @@ -34,8 +35,10 @@ const startManualRecording = defineMutation({ description: 'Setting up your recording environment...', }); // Pause media before starting recording + // Background media pause: do not await; track by sessionId for later resume + currentMediaSessionId = nanoid(); console.time('[startup] media.pauseIfEnabled'); - await media.pauseIfEnabled.execute(undefined); + void media.pauseIfEnabled.execute({ sessionId: currentMediaSessionId }); console.timeEnd('[startup] media.pauseIfEnabled'); console.time('[startup] recorder.startRecording'); @@ -115,8 +118,11 @@ const stopManualRecording = defineMutation({ const { data: blob, error: stopRecordingError } = await recorder.stopRecording.execute({ toastId }); - // Resume media after stopping recording - await media.resumePaused.execute(undefined); + // Resume media for this session + if (currentMediaSessionId) { + await media.resumePaused.execute({ sessionId: currentMediaSessionId }); + currentMediaSessionId = null; + } if (stopRecordingError) { notify.error.execute({ id: toastId, ...stopRecordingError }); return Err(stopRecordingError); @@ -167,7 +173,8 @@ const startVadRecording = defineMutation({ description: 'Your voice activated capture is starting...', }); // Pause media before starting VAD - await media.pauseIfEnabled.execute(undefined); + currentMediaSessionId = nanoid(); + void media.pauseIfEnabled.execute({ sessionId: currentMediaSessionId }); const { data: deviceAcquisitionOutcome, error: startActiveListeningError } = await vadRecorder.startActiveListening.execute({ @@ -274,8 +281,11 @@ const stopVadRecording = defineMutation({ const { error: stopVadError } = await vadRecorder.stopActiveListening.execute(undefined); - // Resume media after stopping VAD - await media.resumePaused.execute(undefined); + // Resume media for this session + if (currentMediaSessionId) { + await media.resumePaused.execute({ sessionId: currentMediaSessionId }); + currentMediaSessionId = null; + } if (stopVadError) { notify.error.execute({ id: toastId, ...stopVadError }); return Err(stopVadError); @@ -326,8 +336,11 @@ export const commands = { const { data: cancelRecordingResult, error: cancelRecordingError } = await recorder.cancelRecording.execute({ toastId }); - // Resume media after canceling recording - await media.resumePaused.execute(undefined); + // Resume media for this session + if (currentMediaSessionId) { + await media.resumePaused.execute({ sessionId: currentMediaSessionId }); + currentMediaSessionId = null; + } if (cancelRecordingError) { notify.error.execute({ id: toastId, ...cancelRecordingError }); return Err(cancelRecordingError); diff --git a/apps/whispering/src/lib/query/media.ts b/apps/whispering/src/lib/query/media.ts index c94eeb9ddf..f87e24d64c 100644 --- a/apps/whispering/src/lib/query/media.ts +++ b/apps/whispering/src/lib/query/media.ts @@ -5,7 +5,7 @@ import { tryAsync, Ok, Err } from 'wellcrafted/result'; type PausedPlayers = { players: string[] }; -let pausedPlayers: string[] = []; +const pausedPlayersBySession = new Map(); async function invoke(command: string, args?: Record) { // Prefer dynamic import to avoid bundling on web @@ -19,10 +19,12 @@ async function invoke(command: string, args?: Record) { export const media = { pauseIfEnabled: defineMutation({ mutationKey: ['media', 'pauseIfEnabled'] as const, - resultMutationFn: async () => { + resultMutationFn: async ({ sessionId }: { sessionId: string }) => { const enabled = settings.value['sound.autoPauseMediaDuringRecording']; if (!enabled || !IS_MACOS || !window.__TAURI_INTERNALS__) return Ok(undefined); + // Fire-and-forget pause; we still await inside this mutation so errors are logged, + // but callers should not await this mutation if they want background behavior. console.info('[media] attempting to pause active media...'); console.time('[media] macos_pause_active_media invoke'); const { data, error } = await invoke('macos_pause_active_media'); @@ -31,20 +33,21 @@ export const media = { console.warn('[media] pause failed', error); return Ok(undefined); } - console.info('[media] paused players:', data.players); - pausedPlayers = data.players ?? []; + const players = data.players ?? []; + console.info('[media] paused players:', players); + pausedPlayersBySession.set(sessionId, players); return Ok(undefined); }, }), resumePaused: defineMutation({ mutationKey: ['media', 'resumePaused'] as const, - resultMutationFn: async () => { + resultMutationFn: async ({ sessionId }: { sessionId: string }) => { if (!IS_MACOS || !window.__TAURI_INTERNALS__) return Ok(undefined); - if (pausedPlayers.length === 0) return Ok(undefined); + const players = pausedPlayersBySession.get(sessionId) ?? []; + if (players.length === 0) return Ok(undefined); - const players = [...pausedPlayers]; - pausedPlayers = []; + pausedPlayersBySession.delete(sessionId); console.info('[media] resuming players:', players); console.time('[media] macos_resume_media invoke'); const { error } = await invoke('macos_resume_media', { players }); From 9f3d65a36c98c82929b707e5258654fba79f6c35 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Fri, 31 Oct 2025 09:14:28 -0700 Subject: [PATCH 20/26] debug --- apps/whispering/src/lib/commands.ts | 30 ++++++++++++++++++++--- apps/whispering/src/lib/query/commands.ts | 6 +++++ apps/whispering/src/lib/query/media.ts | 6 +++++ apps/whispering/src/lib/query/recorder.ts | 3 +++ apps/whispering/src/lib/query/sound.ts | 6 ++++- apps/whispering/src/lib/utils/timing.ts | 24 ++++++++++++++++++ 6 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 apps/whispering/src/lib/utils/timing.ts diff --git a/apps/whispering/src/lib/commands.ts b/apps/whispering/src/lib/commands.ts index aaf9c02d20..4f1261dd59 100644 --- a/apps/whispering/src/lib/commands.ts +++ b/apps/whispering/src/lib/commands.ts @@ -1,4 +1,5 @@ import { rpc } from '$lib/query'; +import { mark } from '$lib/utils/timing'; import type { ShortcutTriggerState } from './services/_shortcut-trigger-state'; type SatisfiedCommand = { @@ -13,19 +14,35 @@ export const commands = [ id: 'pushToTalk', title: 'Push to talk', on: 'Both', - callback: () => rpc.commands.toggleManualRecording.execute(undefined), + callback: () => { + // Mark shortcut start timestamp for end-to-end timing + ;(window as any).__WHISPERING_SHORTCUT_T0 = performance.now(); + ;(window as any).__WHISPERING_TIMINGS = []; + mark('shortcut:pressed', { id: 'pushToTalk' }); + rpc.commands.toggleManualRecording.execute(undefined); + }, }, { id: 'toggleManualRecording', title: 'Toggle recording', on: 'Pressed', - callback: () => rpc.commands.toggleManualRecording.execute(undefined), + callback: () => { + ;(window as any).__WHISPERING_SHORTCUT_T0 = performance.now(); + ;(window as any).__WHISPERING_TIMINGS = []; + mark('shortcut:pressed', { id: 'toggleManualRecording' }); + rpc.commands.toggleManualRecording.execute(undefined); + }, }, { id: 'startManualRecording', title: 'Start recording', on: 'Pressed', - callback: () => rpc.commands.startManualRecording.execute(undefined), + callback: () => { + ;(window as any).__WHISPERING_SHORTCUT_T0 = performance.now(); + ;(window as any).__WHISPERING_TIMINGS = []; + mark('shortcut:pressed', { id: 'startManualRecording' }); + rpc.commands.startManualRecording.execute(undefined); + }, }, { id: 'stopManualRecording', @@ -43,7 +60,12 @@ export const commands = [ id: 'startVadRecording', title: 'Start voice activated recording', on: 'Pressed', - callback: () => rpc.commands.startVadRecording.execute(undefined), + callback: () => { + ;(window as any).__WHISPERING_SHORTCUT_T0 = performance.now(); + ;(window as any).__WHISPERING_TIMINGS = []; + mark('shortcut:pressed', { id: 'startVadRecording' }); + rpc.commands.startVadRecording.execute(undefined); + }, }, { id: 'stopVadRecording', diff --git a/apps/whispering/src/lib/query/commands.ts b/apps/whispering/src/lib/query/commands.ts index 311c94a530..fc7cef437b 100644 --- a/apps/whispering/src/lib/query/commands.ts +++ b/apps/whispering/src/lib/query/commands.ts @@ -34,6 +34,8 @@ const startManualRecording = defineMutation({ title: '๐ŸŽ™๏ธ Preparing to record...', description: 'Setting up your recording environment...', }); + // Point-in-time mark for UI toast + try { (await import('$lib/utils/timing')).mark('ui:toast:prepare'); } catch {} // Pause media before starting recording // Background media pause: do not await; track by sessionId for later resume currentMediaSessionId = nanoid(); @@ -41,10 +43,12 @@ const startManualRecording = defineMutation({ void media.pauseIfEnabled.execute({ sessionId: currentMediaSessionId }); console.timeEnd('[startup] media.pauseIfEnabled'); + try { (await import('$lib/utils/timing')).mark('recorder:start:queued'); } catch {} console.time('[startup] recorder.startRecording'); const { data: deviceAcquisitionOutcome, error: startRecordingError } = await recorder.startRecording.execute({ toastId }); console.timeEnd('[startup] recorder.startRecording'); + try { (await import('$lib/utils/timing')).mark('recorder:start:finished'); } catch {} if (startRecordingError) { notify.error.execute({ id: toastId, ...startRecordingError }); @@ -53,6 +57,7 @@ const startManualRecording = defineMutation({ switch (deviceAcquisitionOutcome.outcome) { case 'success': { + try { (await import('$lib/utils/timing')).mark('record:ready'); } catch {} notify.success.execute({ id: toastId, title: '๐ŸŽ™๏ธ Whispering is recording...', @@ -102,6 +107,7 @@ const startManualRecording = defineMutation({ manualRecordingStartTime = Date.now(); console.info('Recording started'); sound.playSoundIfEnabled.execute('manual-start'); + try { (await import('$lib/utils/timing')).mark('sound:queued', { sound: 'manual-start' }); } catch {} return Ok(undefined); }, }); diff --git a/apps/whispering/src/lib/query/media.ts b/apps/whispering/src/lib/query/media.ts index f87e24d64c..eebb58e17c 100644 --- a/apps/whispering/src/lib/query/media.ts +++ b/apps/whispering/src/lib/query/media.ts @@ -1,4 +1,5 @@ import { defineMutation } from './_client'; +import { mark } from '$lib/utils/timing'; import { settings } from '$lib/stores/settings.svelte'; import { IS_MACOS } from '$lib/constants/platform/is-macos'; import { tryAsync, Ok, Err } from 'wellcrafted/result'; @@ -26,16 +27,19 @@ export const media = { // Fire-and-forget pause; we still await inside this mutation so errors are logged, // but callers should not await this mutation if they want background behavior. console.info('[media] attempting to pause active media...'); + mark('media:pause:begin'); console.time('[media] macos_pause_active_media invoke'); const { data, error } = await invoke('macos_pause_active_media'); console.timeEnd('[media] macos_pause_active_media invoke'); if (error) { console.warn('[media] pause failed', error); + mark('media:pause:end', { error: true }); return Ok(undefined); } const players = data.players ?? []; console.info('[media] paused players:', players); pausedPlayersBySession.set(sessionId, players); + mark('media:pause:end', { players }); return Ok(undefined); }, }), @@ -49,12 +53,14 @@ export const media = { pausedPlayersBySession.delete(sessionId); console.info('[media] resuming players:', players); + mark('media:resume:begin', { players }); console.time('[media] macos_resume_media invoke'); const { error } = await invoke('macos_resume_media', { players }); console.timeEnd('[media] macos_resume_media invoke'); if (error) { console.warn('[media] resume failed', error); } + mark('media:resume:end'); return Ok(undefined); }, }), diff --git a/apps/whispering/src/lib/query/recorder.ts b/apps/whispering/src/lib/query/recorder.ts index fd4b784866..fdecabfe4c 100644 --- a/apps/whispering/src/lib/query/recorder.ts +++ b/apps/whispering/src/lib/query/recorder.ts @@ -12,6 +12,7 @@ import { Ok } from 'wellcrafted/result'; import { defineMutation, defineQuery, queryClient } from './_client'; import { notify } from './notify'; import { nanoid } from 'nanoid/non-secure'; +import { mark } from '$lib/utils/timing'; const recorderKeys = { recorderState: ['recorder', 'recorderState'] as const, @@ -109,11 +110,13 @@ export const recorder = { ]; console.info('[startup] recorder method:', params.method); + mark('recorder:start:begin', { method: params.method }); const { data: deviceAcquisitionOutcome, error: startRecordingError } = await recorderService().startRecording(params, { sendStatus: (options) => notify.loading.execute({ id: toastId, ...options }), }); + mark('recorder:start:end', { method: params.method }); if (startRecordingError) { return fromTaggedErr(startRecordingError, { diff --git a/apps/whispering/src/lib/query/sound.ts b/apps/whispering/src/lib/query/sound.ts index c41d4b7bd1..1469aad4cf 100644 --- a/apps/whispering/src/lib/query/sound.ts +++ b/apps/whispering/src/lib/query/sound.ts @@ -4,6 +4,7 @@ import type { PlaySoundServiceError } from '$lib/services/sound'; import { settings } from '$lib/stores/settings.svelte'; import { Ok, type Result } from 'wellcrafted/result'; import { defineMutation } from './_client'; +import { mark } from '$lib/utils/timing'; const soundKeys = { all: ['sound'] as const, @@ -19,7 +20,10 @@ export const sound = { if (!settings.value[`sound.playOn.${soundName}`]) { return Ok(undefined); } - return await services.sound.playSound(soundName); + mark('sound:begin', { soundName }); + const result = await services.sound.playSound(soundName); + mark('sound:end', { soundName }); + return result; }, }), }; diff --git a/apps/whispering/src/lib/utils/timing.ts b/apps/whispering/src/lib/utils/timing.ts new file mode 100644 index 0000000000..f1001848a3 --- /dev/null +++ b/apps/whispering/src/lib/utils/timing.ts @@ -0,0 +1,24 @@ +// Minimal timing harness for end-to-end, async-aware event logging + +type TimingEvent = { + t: number; // ms since t0 + label: string; + data?: unknown; +}; + +function getT0(): number { + const t0 = (window as any).__WHISPERING_SHORTCUT_T0 as number | undefined; + return typeof t0 === 'number' ? t0 : performance.now(); +} + +export function mark(label: string, data?: unknown) { + const t = performance.now() - getT0(); + const event: TimingEvent = { t, label, data }; + // Structured log for easy scanning + console.info('[timing]', `${t.toFixed(2)}ms`, label, data ?? ''); + // Keep a rolling buffer on window for later inspection + const buf = ((window as any).__WHISPERING_TIMINGS ??= [] as TimingEvent[]); + buf.push(event); +} + + From d51341a37b083648ca03f924824350a6c8583759 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Fri, 31 Oct 2025 09:38:39 -0700 Subject: [PATCH 21/26] cleaning up --- apps/whispering/src-tauri/src/macos_media.rs | 29 ++++--------------- apps/whispering/src/lib/commands.ts | 30 +++----------------- apps/whispering/src/lib/query/commands.ts | 15 ++-------- apps/whispering/src/lib/query/media.ts | 10 ------- apps/whispering/src/lib/query/recorder.ts | 3 -- apps/whispering/src/lib/query/sound.ts | 6 +--- apps/whispering/src/lib/utils/timing.ts | 24 ---------------- 7 files changed, 13 insertions(+), 104 deletions(-) delete mode 100644 apps/whispering/src/lib/utils/timing.ts diff --git a/apps/whispering/src-tauri/src/macos_media.rs b/apps/whispering/src-tauri/src/macos_media.rs index dee4d769a4..61e8f2e936 100644 --- a/apps/whispering/src-tauri/src/macos_media.rs +++ b/apps/whispering/src-tauri/src/macos_media.rs @@ -8,9 +8,6 @@ pub struct PausedPlayers { pub async fn macos_pause_active_media() -> Result { #[cfg(target_os = "macos")] { - use std::time::Instant; - let start = Instant::now(); - // Run Music and Spotify checks concurrently with short AppleScript timeouts let music_script = r#" try @@ -44,35 +41,25 @@ end try return "" "#; - let music_start = Instant::now(); - let spotify_start = Instant::now(); let (music_out, spotify_out) = tokio::join!( async { - let r = run_osascript(music_script).await; - let d = music_start.elapsed(); - (r, d) + run_osascript(music_script).await }, async { - let r = run_osascript(spotify_script).await; - let d = spotify_start.elapsed(); - (r, d) + run_osascript(spotify_script).await } ); - eprintln!("[macos_media] Music check took {:?}", music_out.1); - eprintln!("[macos_media] Spotify check took {:?}", spotify_out.1); - let mut paused_players = Vec::new(); - if let Ok(output) = music_out.0 { + if let Ok(output) = music_out { if !output.trim().is_empty() { paused_players.push(output.trim().to_string()); } } - if let Ok(output) = spotify_out.0 { + if let Ok(output) = spotify_out { if !output.trim().is_empty() { paused_players.push(output.trim().to_string()); } } // Only check Books if nothing else was paused if paused_players.is_empty() { - let books_start = Instant::now(); let books_result = run_osascript(r#" try with timeout of 0.3 seconds @@ -94,19 +81,13 @@ end try return "" "#).await; - let books_time = books_start.elapsed(); - eprintln!("[macos_media] Books check took {:?}", books_time); - if let Ok(output) = books_result { if !output.trim().is_empty() { paused_players.push(output.trim().to_string()); } } } - - let total_time = start.elapsed(); - eprintln!("[macos_media] Total pause took {:?}", total_time); - + return Ok(PausedPlayers { players: paused_players }); } diff --git a/apps/whispering/src/lib/commands.ts b/apps/whispering/src/lib/commands.ts index 4f1261dd59..aaf9c02d20 100644 --- a/apps/whispering/src/lib/commands.ts +++ b/apps/whispering/src/lib/commands.ts @@ -1,5 +1,4 @@ import { rpc } from '$lib/query'; -import { mark } from '$lib/utils/timing'; import type { ShortcutTriggerState } from './services/_shortcut-trigger-state'; type SatisfiedCommand = { @@ -14,35 +13,19 @@ export const commands = [ id: 'pushToTalk', title: 'Push to talk', on: 'Both', - callback: () => { - // Mark shortcut start timestamp for end-to-end timing - ;(window as any).__WHISPERING_SHORTCUT_T0 = performance.now(); - ;(window as any).__WHISPERING_TIMINGS = []; - mark('shortcut:pressed', { id: 'pushToTalk' }); - rpc.commands.toggleManualRecording.execute(undefined); - }, + callback: () => rpc.commands.toggleManualRecording.execute(undefined), }, { id: 'toggleManualRecording', title: 'Toggle recording', on: 'Pressed', - callback: () => { - ;(window as any).__WHISPERING_SHORTCUT_T0 = performance.now(); - ;(window as any).__WHISPERING_TIMINGS = []; - mark('shortcut:pressed', { id: 'toggleManualRecording' }); - rpc.commands.toggleManualRecording.execute(undefined); - }, + callback: () => rpc.commands.toggleManualRecording.execute(undefined), }, { id: 'startManualRecording', title: 'Start recording', on: 'Pressed', - callback: () => { - ;(window as any).__WHISPERING_SHORTCUT_T0 = performance.now(); - ;(window as any).__WHISPERING_TIMINGS = []; - mark('shortcut:pressed', { id: 'startManualRecording' }); - rpc.commands.startManualRecording.execute(undefined); - }, + callback: () => rpc.commands.startManualRecording.execute(undefined), }, { id: 'stopManualRecording', @@ -60,12 +43,7 @@ export const commands = [ id: 'startVadRecording', title: 'Start voice activated recording', on: 'Pressed', - callback: () => { - ;(window as any).__WHISPERING_SHORTCUT_T0 = performance.now(); - ;(window as any).__WHISPERING_TIMINGS = []; - mark('shortcut:pressed', { id: 'startVadRecording' }); - rpc.commands.startVadRecording.execute(undefined); - }, + callback: () => rpc.commands.startVadRecording.execute(undefined), }, { id: 'stopVadRecording', diff --git a/apps/whispering/src/lib/query/commands.ts b/apps/whispering/src/lib/query/commands.ts index fc7cef437b..d9549fd4ac 100644 --- a/apps/whispering/src/lib/query/commands.ts +++ b/apps/whispering/src/lib/query/commands.ts @@ -24,9 +24,7 @@ let currentMediaSessionId: string | null = null; const startManualRecording = defineMutation({ mutationKey: ['commands', 'startManualRecording'] as const, resultMutationFn: async () => { - console.time('[startup] switchRecordingMode(manual)'); await settings.switchRecordingMode('manual'); - console.timeEnd('[startup] switchRecordingMode(manual)'); const toastId = nanoid(); notify.loading.execute({ @@ -34,21 +32,15 @@ const startManualRecording = defineMutation({ title: '๐ŸŽ™๏ธ Preparing to record...', description: 'Setting up your recording environment...', }); - // Point-in-time mark for UI toast - try { (await import('$lib/utils/timing')).mark('ui:toast:prepare'); } catch {} + // Show preparing toast // Pause media before starting recording // Background media pause: do not await; track by sessionId for later resume currentMediaSessionId = nanoid(); - console.time('[startup] media.pauseIfEnabled'); void media.pauseIfEnabled.execute({ sessionId: currentMediaSessionId }); - console.timeEnd('[startup] media.pauseIfEnabled'); - try { (await import('$lib/utils/timing')).mark('recorder:start:queued'); } catch {} - console.time('[startup] recorder.startRecording'); + // Start recording immediately; media pause runs in background const { data: deviceAcquisitionOutcome, error: startRecordingError } = await recorder.startRecording.execute({ toastId }); - console.timeEnd('[startup] recorder.startRecording'); - try { (await import('$lib/utils/timing')).mark('recorder:start:finished'); } catch {} if (startRecordingError) { notify.error.execute({ id: toastId, ...startRecordingError }); @@ -57,7 +49,7 @@ const startManualRecording = defineMutation({ switch (deviceAcquisitionOutcome.outcome) { case 'success': { - try { (await import('$lib/utils/timing')).mark('record:ready'); } catch {} + // Record ready notify.success.execute({ id: toastId, title: '๐ŸŽ™๏ธ Whispering is recording...', @@ -107,7 +99,6 @@ const startManualRecording = defineMutation({ manualRecordingStartTime = Date.now(); console.info('Recording started'); sound.playSoundIfEnabled.execute('manual-start'); - try { (await import('$lib/utils/timing')).mark('sound:queued', { sound: 'manual-start' }); } catch {} return Ok(undefined); }, }); diff --git a/apps/whispering/src/lib/query/media.ts b/apps/whispering/src/lib/query/media.ts index eebb58e17c..d296877e79 100644 --- a/apps/whispering/src/lib/query/media.ts +++ b/apps/whispering/src/lib/query/media.ts @@ -1,5 +1,4 @@ import { defineMutation } from './_client'; -import { mark } from '$lib/utils/timing'; import { settings } from '$lib/stores/settings.svelte'; import { IS_MACOS } from '$lib/constants/platform/is-macos'; import { tryAsync, Ok, Err } from 'wellcrafted/result'; @@ -27,19 +26,14 @@ export const media = { // Fire-and-forget pause; we still await inside this mutation so errors are logged, // but callers should not await this mutation if they want background behavior. console.info('[media] attempting to pause active media...'); - mark('media:pause:begin'); - console.time('[media] macos_pause_active_media invoke'); const { data, error } = await invoke('macos_pause_active_media'); - console.timeEnd('[media] macos_pause_active_media invoke'); if (error) { console.warn('[media] pause failed', error); - mark('media:pause:end', { error: true }); return Ok(undefined); } const players = data.players ?? []; console.info('[media] paused players:', players); pausedPlayersBySession.set(sessionId, players); - mark('media:pause:end', { players }); return Ok(undefined); }, }), @@ -53,14 +47,10 @@ export const media = { pausedPlayersBySession.delete(sessionId); console.info('[media] resuming players:', players); - mark('media:resume:begin', { players }); - console.time('[media] macos_resume_media invoke'); const { error } = await invoke('macos_resume_media', { players }); - console.timeEnd('[media] macos_resume_media invoke'); if (error) { console.warn('[media] resume failed', error); } - mark('media:resume:end'); return Ok(undefined); }, }), diff --git a/apps/whispering/src/lib/query/recorder.ts b/apps/whispering/src/lib/query/recorder.ts index fdecabfe4c..fd4b784866 100644 --- a/apps/whispering/src/lib/query/recorder.ts +++ b/apps/whispering/src/lib/query/recorder.ts @@ -12,7 +12,6 @@ import { Ok } from 'wellcrafted/result'; import { defineMutation, defineQuery, queryClient } from './_client'; import { notify } from './notify'; import { nanoid } from 'nanoid/non-secure'; -import { mark } from '$lib/utils/timing'; const recorderKeys = { recorderState: ['recorder', 'recorderState'] as const, @@ -110,13 +109,11 @@ export const recorder = { ]; console.info('[startup] recorder method:', params.method); - mark('recorder:start:begin', { method: params.method }); const { data: deviceAcquisitionOutcome, error: startRecordingError } = await recorderService().startRecording(params, { sendStatus: (options) => notify.loading.execute({ id: toastId, ...options }), }); - mark('recorder:start:end', { method: params.method }); if (startRecordingError) { return fromTaggedErr(startRecordingError, { diff --git a/apps/whispering/src/lib/query/sound.ts b/apps/whispering/src/lib/query/sound.ts index 1469aad4cf..c41d4b7bd1 100644 --- a/apps/whispering/src/lib/query/sound.ts +++ b/apps/whispering/src/lib/query/sound.ts @@ -4,7 +4,6 @@ import type { PlaySoundServiceError } from '$lib/services/sound'; import { settings } from '$lib/stores/settings.svelte'; import { Ok, type Result } from 'wellcrafted/result'; import { defineMutation } from './_client'; -import { mark } from '$lib/utils/timing'; const soundKeys = { all: ['sound'] as const, @@ -20,10 +19,7 @@ export const sound = { if (!settings.value[`sound.playOn.${soundName}`]) { return Ok(undefined); } - mark('sound:begin', { soundName }); - const result = await services.sound.playSound(soundName); - mark('sound:end', { soundName }); - return result; + return await services.sound.playSound(soundName); }, }), }; diff --git a/apps/whispering/src/lib/utils/timing.ts b/apps/whispering/src/lib/utils/timing.ts deleted file mode 100644 index f1001848a3..0000000000 --- a/apps/whispering/src/lib/utils/timing.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Minimal timing harness for end-to-end, async-aware event logging - -type TimingEvent = { - t: number; // ms since t0 - label: string; - data?: unknown; -}; - -function getT0(): number { - const t0 = (window as any).__WHISPERING_SHORTCUT_T0 as number | undefined; - return typeof t0 === 'number' ? t0 : performance.now(); -} - -export function mark(label: string, data?: unknown) { - const t = performance.now() - getT0(); - const event: TimingEvent = { t, label, data }; - // Structured log for easy scanning - console.info('[timing]', `${t.toFixed(2)}ms`, label, data ?? ''); - // Keep a rolling buffer on window for later inspection - const buf = ((window as any).__WHISPERING_TIMINGS ??= [] as TimingEvent[]); - buf.push(event); -} - - From 7c1aab16c262d0f963737f645606cd3e9110e1c4 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Fri, 31 Oct 2025 09:49:08 -0700 Subject: [PATCH 22/26] Update macos_media.rs --- apps/whispering/src-tauri/src/macos_media.rs | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/apps/whispering/src-tauri/src/macos_media.rs b/apps/whispering/src-tauri/src/macos_media.rs index 61e8f2e936..6a4e60fff0 100644 --- a/apps/whispering/src-tauri/src/macos_media.rs +++ b/apps/whispering/src-tauri/src/macos_media.rs @@ -11,7 +11,7 @@ pub async fn macos_pause_active_media() -> Result { // Run Music and Spotify checks concurrently with short AppleScript timeouts let music_script = r#" try - with timeout of 0.2 seconds + with timeout of 1.0 seconds tell application "Music" if it is running then if player state is playing then @@ -27,7 +27,7 @@ return "" let spotify_script = r#" try - with timeout of 0.2 seconds + with timeout of 1.0 seconds tell application "Spotify" if it is running then if player state is playing then @@ -157,16 +157,3 @@ async fn run_osascript(script: &str) -> Result { } } -fn parse_comma_list(s: &str) -> Vec { - let trimmed = s.trim(); - if trimmed.is_empty() { - return vec![]; - } - trimmed - .split(',') - .map(|p| p.trim().to_string()) - .filter(|p| !p.is_empty()) - .collect() -} - - From d073110f11f037f1a8c2a21d352ffd14e7b4fcd8 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Fri, 31 Oct 2025 09:56:57 -0700 Subject: [PATCH 23/26] -c --- apps/whispering/src/lib/query/commands.ts | 5 ++--- apps/whispering/src/lib/query/recorder.ts | 3 --- apps/whispering/src/routes/(config)/settings/+page.svelte | 1 - 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/apps/whispering/src/lib/query/commands.ts b/apps/whispering/src/lib/query/commands.ts index d9549fd4ac..b4ddaf9486 100644 --- a/apps/whispering/src/lib/query/commands.ts +++ b/apps/whispering/src/lib/query/commands.ts @@ -49,7 +49,6 @@ const startManualRecording = defineMutation({ switch (deviceAcquisitionOutcome.outcome) { case 'success': { - // Record ready notify.success.execute({ id: toastId, title: '๐ŸŽ™๏ธ Whispering is recording...', @@ -139,7 +138,7 @@ const stopManualRecording = defineMutation({ duration = Date.now() - manualRecordingStartTime; manualRecordingStartTime = null; // Reset for next recording } - rpc.analytics.logEvent.execute({ + rpc.analytics.logEvent.execute({ type: 'manual_recording_completed', blob_size: blob.size, duration, @@ -192,7 +191,7 @@ const startVadRecording = defineMutation({ sound.playSoundIfEnabled.execute('vad-capture'); // Log VAD recording completion - rpc.analytics.logEvent.execute({ + rpc.analytics.logEvent.execute({ type: 'vad_recording_completed', blob_size: blob.size, // VAD doesn't track duration by default diff --git a/apps/whispering/src/lib/query/recorder.ts b/apps/whispering/src/lib/query/recorder.ts index fd4b784866..49640098c6 100644 --- a/apps/whispering/src/lib/query/recorder.ts +++ b/apps/whispering/src/lib/query/recorder.ts @@ -69,12 +69,10 @@ export const recorder = { }; // Resolve the output folder - use default if null - console.time('[startup] resolveOutputFolder'); const outputFolder = window.__TAURI_INTERNALS__ ? (settings.value['recording.cpal.outputFolder'] ?? (await getDefaultRecordingsFolder())) : ''; - console.timeEnd('[startup] resolveOutputFolder'); const paramsMap = { navigator: { @@ -108,7 +106,6 @@ export const recorder = { : settings.value['recording.method'] ]; - console.info('[startup] recorder method:', params.method); const { data: deviceAcquisitionOutcome, error: startRecordingError } = await recorderService().startRecording(params, { sendStatus: (options) => diff --git a/apps/whispering/src/routes/(config)/settings/+page.svelte b/apps/whispering/src/routes/(config)/settings/+page.svelte index f251693a86..ed9dc8c3a8 100644 --- a/apps/whispering/src/routes/(config)/settings/+page.svelte +++ b/apps/whispering/src/routes/(config)/settings/+page.svelte @@ -109,6 +109,5 @@ } placeholder="Select a language" /> - {/if} From b255a932fe9ad23c347c612634f54a04ae78e7d6 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Thu, 4 Dec 2025 17:57:38 -0800 Subject: [PATCH 24/26] Update tauri.conf.json --- apps/whispering/src-tauri/tauri.conf.json | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/whispering/src-tauri/tauri.conf.json b/apps/whispering/src-tauri/tauri.conf.json index 423665e3d8..edc9f4923c 100644 --- a/apps/whispering/src-tauri/tauri.conf.json +++ b/apps/whispering/src-tauri/tauri.conf.json @@ -38,7 +38,6 @@ "type": "embedBootstrapper" }, "wix": { - "fipsCompliant": true, "language": "en-US" }, "nsis": { From 47cd0709deae4051caa5b11161f88b309940bbb7 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Thu, 4 Dec 2025 17:58:22 -0800 Subject: [PATCH 25/26] Update bun.lock --- bun.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/bun.lock b/bun.lock index c835f3bc5a..b3e9f14eb2 100644 --- a/bun.lock +++ b/bun.lock @@ -1,6 +1,5 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "epicenter", From 0574729dee8f3c6b44bda557829eda80cd151595 Mon Sep 17 00:00:00 2001 From: Dave2 Buchanan <146779710+unlox775-code-dot-org@users.noreply.github.com> Date: Fri, 5 Dec 2025 13:14:50 -0800 Subject: [PATCH 26/26] Lint Fixes --- apps/whispering/src/lib/query/actions.ts | 27 ++++-- apps/whispering/src/lib/query/index.ts | 2 +- apps/whispering/src/lib/query/media.ts | 98 ++++++++++---------- apps/whispering/src/lib/settings/settings.ts | 2 - 4 files changed, 68 insertions(+), 61 deletions(-) diff --git a/apps/whispering/src/lib/query/actions.ts b/apps/whispering/src/lib/query/actions.ts index bf5eebac2b..381fb797e7 100644 --- a/apps/whispering/src/lib/query/actions.ts +++ b/apps/whispering/src/lib/query/actions.ts @@ -1,6 +1,10 @@ import { nanoid } from 'nanoid/non-secure'; import { Err, Ok } from 'wellcrafted/result'; -import { fromTaggedError, WhisperingErr } from '$lib/result'; +import { + fromTaggedError, + WhisperingErr, + type WhisperingError, +} from '$lib/result'; import { DbServiceErr } from '$lib/services/db'; import { settings } from '$lib/stores/settings.svelte'; import * as transformClipboardWindow from '../../routes/transform-clipboard/transformClipboardWindow.tauri'; @@ -8,6 +12,7 @@ import { rpc } from './'; import { defineMutation } from './_client'; import { db } from './db'; import { delivery } from './delivery'; +import { media } from './media'; import { notify } from './notify'; import { recorder } from './recorder'; import { sound } from './sound'; @@ -15,7 +20,6 @@ import { text } from './text'; import { transcription } from './transcription'; import { transformer } from './transformer'; import { vadRecorder } from './vad.svelte'; -import { media } from './media'; /** * Application actions. These are mutations at the UI boundary that can be invoked @@ -219,7 +223,7 @@ const startVadRecording = defineMutation({ // Pause media before starting VAD currentMediaSessionId = nanoid(); void media.pauseIfEnabled.execute({ sessionId: currentMediaSessionId }); - + const { data: deviceAcquisitionOutcome, error: startActiveListeningError } = await vadRecorder.startActiveListening({ onSpeechStart: () => { @@ -255,12 +259,12 @@ const startVadRecording = defineMutation({ }, }); if (startActiveListeningError) { - const errAny = startActiveListeningError as any; + const error = startActiveListeningError as WhisperingError; notify.error.execute({ id: toastId, - title: errAny?.title ?? 'โŒ Failed to start VAD', + title: error?.title ?? 'โŒ Failed to start VAD', description: - errAny?.description ?? + error?.description ?? 'Voice activity detection could not be started.', action: { type: 'more-details', error: startActiveListeningError }, }); @@ -340,12 +344,12 @@ const stopVadRecording = defineMutation({ currentMediaSessionId = null; } if (stopVadError) { - const errAny = stopVadError as any; + const error = stopVadError as WhisperingError; notify.error.execute({ id: toastId, - title: errAny?.title ?? 'โŒ Failed to stop VAD', + title: error?.title ?? 'โŒ Failed to stop VAD', description: - errAny?.description ?? + error?.description ?? 'Voice activity detection could not be stopped.', action: { type: 'more-details', error: stopVadError }, }); @@ -449,7 +453,10 @@ export const commands = { toggleVadRecording: defineMutation({ mutationKey: ['commands', 'toggleVadRecording'] as const, resultMutationFn: async () => { - if (vadRecorder.state === 'LISTENING' || vadRecorder.state === 'SPEECH_DETECTED') { + if ( + vadRecorder.state === 'LISTENING' || + vadRecorder.state === 'SPEECH_DETECTED' + ) { return await stopVadRecording.execute(undefined); } return await startVadRecording.execute(undefined); diff --git a/apps/whispering/src/lib/query/index.ts b/apps/whispering/src/lib/query/index.ts index 2ed7f89538..479bd3f9f7 100644 --- a/apps/whispering/src/lib/query/index.ts +++ b/apps/whispering/src/lib/query/index.ts @@ -6,6 +6,7 @@ import { db } from './db'; import { delivery } from './delivery'; import { download } from './download'; import { ffmpeg } from './ffmpeg'; +import { media } from './media'; import { notify } from './notify'; import { recorder } from './recorder'; import { shortcuts } from './shortcuts'; @@ -14,7 +15,6 @@ import { text } from './text'; import { transcription } from './transcription'; import { transformer } from './transformer'; import { tray } from './tray'; -import { media } from './media'; /** * Unified namespace for all query operations. diff --git a/apps/whispering/src/lib/query/media.ts b/apps/whispering/src/lib/query/media.ts index d296877e79..9051a2ea58 100644 --- a/apps/whispering/src/lib/query/media.ts +++ b/apps/whispering/src/lib/query/media.ts @@ -1,59 +1,61 @@ -import { defineMutation } from './_client'; -import { settings } from '$lib/stores/settings.svelte'; +import { Err, Ok, tryAsync } from 'wellcrafted/result'; import { IS_MACOS } from '$lib/constants/platform/is-macos'; -import { tryAsync, Ok, Err } from 'wellcrafted/result'; +import { settings } from '$lib/stores/settings.svelte'; +import { defineMutation } from './_client'; type PausedPlayers = { players: string[] }; const pausedPlayersBySession = new Map(); async function invoke(command: string, args?: Record) { - // Prefer dynamic import to avoid bundling on web - const { invoke } = await import('@tauri-apps/api/core'); - return tryAsync({ - try: async () => await invoke(command, args), - catch: (error) => Err({ name: 'TauriInvokeError', command, error } as const), - }); + // Prefer dynamic import to avoid bundling on web + const { invoke } = await import('@tauri-apps/api/core'); + return tryAsync({ + try: async () => await invoke(command, args), + catch: (error) => + Err({ name: 'TauriInvokeError', command, error } as const), + }); } export const media = { - pauseIfEnabled: defineMutation({ - mutationKey: ['media', 'pauseIfEnabled'] as const, - resultMutationFn: async ({ sessionId }: { sessionId: string }) => { - const enabled = settings.value['sound.autoPauseMediaDuringRecording']; - if (!enabled || !IS_MACOS || !window.__TAURI_INTERNALS__) return Ok(undefined); - - // Fire-and-forget pause; we still await inside this mutation so errors are logged, - // but callers should not await this mutation if they want background behavior. - console.info('[media] attempting to pause active media...'); - const { data, error } = await invoke('macos_pause_active_media'); - if (error) { - console.warn('[media] pause failed', error); - return Ok(undefined); - } - const players = data.players ?? []; - console.info('[media] paused players:', players); - pausedPlayersBySession.set(sessionId, players); - return Ok(undefined); - }, - }), - - resumePaused: defineMutation({ - mutationKey: ['media', 'resumePaused'] as const, - resultMutationFn: async ({ sessionId }: { sessionId: string }) => { - if (!IS_MACOS || !window.__TAURI_INTERNALS__) return Ok(undefined); - const players = pausedPlayersBySession.get(sessionId) ?? []; - if (players.length === 0) return Ok(undefined); - - pausedPlayersBySession.delete(sessionId); - console.info('[media] resuming players:', players); - const { error } = await invoke('macos_resume_media', { players }); - if (error) { - console.warn('[media] resume failed', error); - } - return Ok(undefined); - }, - }), + pauseIfEnabled: defineMutation({ + mutationKey: ['media', 'pauseIfEnabled'] as const, + resultMutationFn: async ({ sessionId }: { sessionId: string }) => { + const enabled = settings.value['sound.autoPauseMediaDuringRecording']; + if (!enabled || !IS_MACOS || !window.__TAURI_INTERNALS__) + return Ok(undefined); + + // Fire-and-forget pause; we still await inside this mutation so errors are logged, + // but callers should not await this mutation if they want background behavior. + console.info('[media] attempting to pause active media...'); + const { data, error } = await invoke( + 'macos_pause_active_media', + ); + if (error) { + console.warn('[media] pause failed', error); + return Ok(undefined); + } + const players = data.players ?? []; + console.info('[media] paused players:', players); + pausedPlayersBySession.set(sessionId, players); + return Ok(undefined); + }, + }), + + resumePaused: defineMutation({ + mutationKey: ['media', 'resumePaused'] as const, + resultMutationFn: async ({ sessionId }: { sessionId: string }) => { + if (!IS_MACOS || !window.__TAURI_INTERNALS__) return Ok(undefined); + const players = pausedPlayersBySession.get(sessionId) ?? []; + if (players.length === 0) return Ok(undefined); + + pausedPlayersBySession.delete(sessionId); + console.info('[media] resuming players:', players); + const { error } = await invoke('macos_resume_media', { players }); + if (error) { + console.warn('[media] resume failed', error); + } + return Ok(undefined); + }, + }), }; - - diff --git a/apps/whispering/src/lib/settings/settings.ts b/apps/whispering/src/lib/settings/settings.ts index b8c930964b..c343d7bb86 100644 --- a/apps/whispering/src/lib/settings/settings.ts +++ b/apps/whispering/src/lib/settings/settings.ts @@ -30,7 +30,6 @@ */ import { type } from 'arktype'; -import type { Command } from '$lib/commands'; import { BITRATES_KBPS, DEFAULT_BITRATE_KBPS, @@ -38,7 +37,6 @@ import { } from '$lib/constants/audio'; import { CommandOrAlt, CommandOrControl } from '$lib/constants/keyboard'; import { SUPPORTED_LANGUAGES } from '$lib/constants/languages'; -import type { WhisperingSoundNames } from '$lib/constants/sounds'; import { ALWAYS_ON_TOP_MODES, LAYOUT_MODES } from '$lib/constants/ui'; import { FFMPEG_DEFAULT_COMPRESSION_OPTIONS,