# Audio History, API Fallback, and Streaming Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Retain the last 10 recordings, export them through a native save dialog, fall back from API to local Whisper PT-BR, and insert streaming API text progressively. **Architecture:** Add a small `audio_history` module that owns safe filename validation, metadata, retention, and export source resolution. Extend the existing OpenAI-compatible client to detect SSE/NDJSON by response `Content-Type`; the existing UI command pipeline owns partial rendering and global-hotkey insertion so the client remains transport-focused. **Tech Stack:** Rust, Tauri 2, `tauri-plugin-dialog`, reqwest blocking client, TypeScript, Vitest, Cargo tests. --- ### Task 1: Persist a bounded, safe audio history **Files:** - Create: `src-tauri/src/audio_history.rs` - Modify: `src-tauri/src/lib.rs` - Modify: `src-tauri/src/app_state.rs` - Modify: `src-tauri/src/ui_commands.rs` - Test: `src-tauri/src/audio_history.rs` - [ ] **Step 1: Write failing history tests** ```rust #[test] fn retain_keeps_the_ten_newest_wavs() { /* create 11 timestamped WAV files; assert 10 remain */ } #[test] fn audio_id_cannot_escape_recordings_dir() { assert!(resolve_audio_path(Path::new("recordings"), "../secret.wav").is_err()); } ``` - [ ] **Step 2: Run the focused test** Run: `cargo test --manifest-path src-tauri/Cargo.toml audio_history::tests --lib` Expected: compilation failure because `audio_history` does not exist. - [ ] **Step 3: Implement a minimal history module** ```rust pub const AUDIO_HISTORY_LIMIT: usize = 10; #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct AudioHistoryItem { pub id: String, pub size_bytes: u64, pub modified_at_ms: u64 } pub fn retain_audio_history(dir: &Path) -> AppResult> { /* list WAVs, sort newest-first, remove beyond 10 */ } pub fn resolve_audio_path(dir: &Path, id: &str) -> AppResult { /* filename-only + canonicalized containment */ } ``` - [ ] **Step 4: Call retention after both normal and hotkey transcription flows** Replace `cleanup_audio_if_needed` calls with `retain_audio_history(&state.recordings_dir)` so captured WAVs are never deleted simply because `keep_audio` is false. Add `audio_history` to `AppSnapshot` from `ManagedAppState::snapshot`. - [ ] **Step 5: Run the focused test** Run: `cargo test --manifest-path src-tauri/Cargo.toml audio_history::tests --lib` Expected: PASS. ### Task 2: Export recordings safely through a native dialog **Files:** - Modify: `src-tauri/Cargo.toml` - Modify: `src-tauri/src/lib.rs` - Modify: `src-tauri/src/ui_commands.rs` - Test: `src-tauri/src/audio_history.rs` - [ ] **Step 1: Add a failing source-resolution test** ```rust #[test] fn resolve_audio_path_accepts_a_listed_wav() { /* assert a known WAV resolves inside recordings */ } ``` - [ ] **Step 2: Add the documented Tauri dialog plugin** ```toml tauri-plugin-dialog = "2" ``` ```rust .plugin(tauri_plugin_dialog::init()) ``` Use `DialogExt::dialog().file().add_filter("Áudio WAV", &["wav"]).blocking_save_file()` and `std::fs::copy`; return `false` when the user cancels. - [ ] **Step 3: Register an explicit command** ```rust #[tauri::command] pub(crate) fn download_audio(id: String, app: tauri::AppHandle, state: State<'_, ManagedAppState>) -> Result ``` Resolve `id` through `audio_history::resolve_audio_path`; never accept a caller-provided file path. - [ ] **Step 4: Run Rust tests** Run: `cargo test --manifest-path src-tauri/Cargo.toml --lib` Expected: PASS. ### Task 3: API-to-Whisper PT-BR fallback **Files:** - Modify: `src-tauri/src/ui_commands.rs` - Test: `src-tauri/src/ui_commands.rs` - [ ] **Step 1: Write a failing pure routing test** ```rust #[test] fn api_failure_without_partial_uses_local_fallback() { assert!(should_fallback_to_whisper(TranscriptionMode::ApiOnly, false)); } #[test] fn partial_stream_does_not_fallback_and_duplicate_text() { assert!(!should_fallback_to_whisper(TranscriptionMode::ApiOnly, true)); } ``` - [ ] **Step 2: Implement the smallest routing rule** ```rust fn should_fallback_to_whisper(mode: TranscriptionMode, partial_received: bool) -> bool { matches!(mode, TranscriptionMode::ApiOnly) && !partial_received } ``` In `run_configured_transcription`, use a cloned settings value with `transcription.language = Some("pt".into())` to invoke existing Whisper after API failure. Preserve local-first’s current local-then-API behavior. - [ ] **Step 3: Run focused tests** Run: `cargo test --manifest-path src-tauri/Cargo.toml ui_commands::tests --lib` Expected: PASS. ### Task 4: Stream response handling and immediate insertion **Files:** - Modify: `src-tauri/src/transcription/openai.rs` - Modify: `src-tauri/src/ui_commands.rs` - Test: `src-tauri/src/transcription/openai.rs` - [ ] **Step 1: Write failing parser tests** ```rust #[test] fn streaming_content_type_is_detected() { /* text/event-stream and x-ndjson true */ } #[test] fn cumulative_stream_text_becomes_only_the_new_suffix() { assert_eq!(stream_delta("olá", "olá mundo"), " mundo"); } ``` - [ ] **Step 2: Implement response-stream transport** Set `Accept: text/event-stream, application/x-ndjson, application/json`. For SSE/NDJSON headers, read lines from the blocking response, ignore `[DONE]`, extract `delta`, `text`, `transcript`, `result`, `output`, `data`, and OpenAI `choices[0].delta.content`, then invoke an optional partial callback. Keep JSON/plain-text behavior unchanged. - [ ] **Step 3: Wire partial callbacks into UI commands** - Main-window transcriptions append partials to `TranscriptDraft.text` and emit snapshots. - Global-hotkey transcriptions hide the non-focusable island on first partial and use existing `FallbackInjector` with `ClipboardPaste` for each new suffix. - Only perform the final full-text insertion when no partial was inserted. - Suppress API-to-Whisper fallback after a partial to prevent duplicate external text. - [ ] **Step 4: Run focused tests** Run: `cargo test --manifest-path src-tauri/Cargo.toml transcription::openai::tests ui_commands::tests --lib` Expected: PASS. ### Task 5: Minimal audio history UI **Files:** - Modify: `src/app.ts` - Modify: `src/app.test.ts` - Modify: `src/styles.css` - [ ] **Step 1: Write failing frontend tests** ```ts it("shows the latest audio and requests its download", async () => { // Fake snapshot has audioHistory[0]; click the button; expect download_audio with its id. }); ``` - [ ] **Step 2: Implement a compact details list** Add `audioHistory` to frontend `AppSnapshot`. Render one collapsed `
` section below diagnostics: “Áudios recentes”, a primary “Baixar último áudio” action, and up to 10 safe metadata rows. Show a useful empty state. Call native `download_audio` by item ID. - [ ] **Step 3: Add restrained CSS** Use existing tokens and compact buttons. Do not introduce a new modal, gradients, or a card grid. Preserve visible focus styles and reduced-motion behavior. - [ ] **Step 4: Run focused tests** Run: `npm test -- --run src/app.test.ts --reporter=verbose` Expected: PASS. ### Task 6: Full verification and release artifact **Files:** - Modify only release metadata if a new artifact version is required. - [ ] Run `cargo fmt --manifest-path src-tauri/Cargo.toml -- --check`. - [ ] Run `cargo test --manifest-path src-tauri/Cargo.toml --lib`. - [ ] Run `npm test -- --run && npm run build`. - [ ] Run `cargo check --manifest-path src-tauri/Cargo.toml` inside the VS Build Tools environment. - [ ] Run `npm run tauri:build` inside the VS Build Tools environment and verify installer timestamps.