fix(dictation): PR 1.4 review polish — banner re-prompt, nav flush, language restore

- Track draft-handled-this-session flag so Restore/Discard suppresses
  the recovery banner on page-nav round-trips within one session;
  resets cleanly on the next successful save.
- Flush pending autosave in onDestroy so Tauri SPA navigation
  preserves the last <1.5s of input.
- Round-trip language on Restore so a draft captured under one
  language doesn't silently inherit the current setting on revival.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 18:44:21 +01:00
parent e3292bd597
commit 20adf73b2a

View File

@@ -1,10 +1,23 @@
<script lang="ts" module>
// PR 1.4 review polish: module-scoped (NOT component-scoped) so the
// flag survives Tauri SPA navigation re-mounting this page. Once the
// user has explicitly Restored or Discarded the recovery banner this
// session, suppress re-prompting on subsequent in-app navigations
// (Dictation → History → Dictation). Without this, the autosave that
// immediately follows a Restore would re-trigger the banner on the
// next visit. Reset to false on the next successful save (or Clear)
// so a fresh capture-and-recovery cycle still works within the same
// session without needing a full app restart.
let draftHandledThisSession = false;
</script>
<script lang="ts"> <script lang="ts">
// @ts-nocheck // @ts-nocheck
import { onMount, onDestroy } from "svelte"; import { onMount, onDestroy } from "svelte";
import { Channel, invoke } from "@tauri-apps/api/core"; import { Channel, invoke } from "@tauri-apps/api/core";
import { emit } from "@tauri-apps/api/event"; import { emit } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window"; import { getCurrentWindow } from "@tauri-apps/api/window";
import { page, settings, templates, profiles, addToHistory, addTask, tasks } from "$lib/stores/page.svelte.js"; import { page, settings, saveSettings, templates, profiles, addToHistory, addTask, tasks } from "$lib/stores/page.svelte.js";
import { markError, markGenerating, markGenerationDone, markLoading, refreshLlmStatus } from "$lib/stores/llmStatus.svelte.js"; import { markError, markGenerating, markGenerationDone, markLoading, refreshLlmStatus } from "$lib/stores/llmStatus.svelte.js";
import { playStartCue, playStopCue, playCompleteCue } from "$lib/utils/sounds.js"; import { playStartCue, playStopCue, playCompleteCue } from "$lib/utils/sounds.js";
import { profilesStore } from "$lib/stores/profiles.svelte.ts"; import { profilesStore } from "$lib/stores/profiles.svelte.ts";
@@ -91,11 +104,15 @@
// Recovery check — runs even in non-Tauri preview so the browser // Recovery check — runs even in non-Tauri preview so the browser
// shell can also offer to restore. Only shows the banner if there's // shell can also offer to restore. Only shows the banner if there's
// nothing currently in the textarea (otherwise the user has live // nothing currently in the textarea (otherwise the user has live
// content and a banner would be confusing). // content and a banner would be confusing). Also skipped if the
const draft = loadRecoverableDraft(); // user already Restored/Discarded this session — see
if (draft && !transcript.trim()) { // draftHandledThisSession above.
recoverableDraft = draft; if (!draftHandledThisSession) {
recoverableDraftAgo = formatTimeAgo(draft.updatedAt); const draft = loadRecoverableDraft();
if (draft && !transcript.trim()) {
recoverableDraft = draft;
recoverableDraftAgo = formatTimeAgo(draft.updatedAt);
}
} }
// visibilitychange + pagehide flush the autosave immediately so // visibilitychange + pagehide flush the autosave immediately so
@@ -125,10 +142,11 @@
visibilityHandler = null; visibilityHandler = null;
} }
window.removeEventListener("pagehide", flushAutosave); window.removeEventListener("pagehide", flushAutosave);
if (autosaveTimer) { // Tauri SPA navigation (Dictation → History → Files) doesn't fire
clearTimeout(autosaveTimer); // pagehide or visibilitychange, so without this flush the last
autosaveTimer = null; // <1.5s of pre-nav input would be lost from the draft. flushAutosave
} // also clears the timer, so no need to clear it again below.
flushAutosave();
clearInterval(timerInterval); clearInterval(timerInterval);
if (page.recording) { if (page.recording) {
page.recording = false; page.recording = false;
@@ -194,8 +212,16 @@
segments = Array.isArray(recoverableDraft.segments) segments = Array.isArray(recoverableDraft.segments)
? [...recoverableDraft.segments] ? [...recoverableDraft.segments]
: []; : [];
// Round-trip the language so a draft captured under a different
// language (e.g. user was in Spanish, switched away, came back)
// doesn't silently inherit the current setting on revival.
if (recoverableDraft.language) {
settings.language = recoverableDraft.language;
saveSettings();
}
recoverableDraft = null; recoverableDraft = null;
recoverableDraftAgo = ""; recoverableDraftAgo = "";
draftHandledThisSession = true;
// Don't clear the localStorage entry — the autosave $effect will // Don't clear the localStorage entry — the autosave $effect will
// immediately rewrite it with a fresh `updatedAt` reflecting the // immediately rewrite it with a fresh `updatedAt` reflecting the
// restored state. If the app crashes between restore and next // restored state. If the app crashes between restore and next
@@ -205,6 +231,7 @@
function discardDraft() { function discardDraft() {
recoverableDraft = null; recoverableDraft = null;
recoverableDraftAgo = ""; recoverableDraftAgo = "";
draftHandledThisSession = true;
clearDraft(); clearDraft();
} }
@@ -734,6 +761,10 @@
return; return;
} }
clearDraft(); clearDraft();
// Successful save closes the current draft cycle; clear the
// session flag so a fresh capture-and-recovery cycle works
// normally without needing a full app restart.
draftHandledThisSession = false;
// Extract tasks from transcript // Extract tasks from transcript
const extracted = await extractTasksForTranscript(transcript); const extracted = await extractTasksForTranscript(transcript);
@@ -799,6 +830,7 @@
// throw away. // throw away.
recoverableDraft = null; recoverableDraft = null;
recoverableDraftAgo = ""; recoverableDraftAgo = "";
draftHandledThisSession = false;
clearDraft(); clearDraft();
} }
@@ -876,6 +908,7 @@
return; return;
} }
clearDraft(); clearDraft();
draftHandledThisSession = false;
saved = true; saved = true;
setTimeout(() => { saved = false; }, FEEDBACK_TIMEOUT_MS); setTimeout(() => { saved = false; }, FEEDBACK_TIMEOUT_MS);
} }