feat(dictation): PR 1.4 — draft recovery + addToHistory persistence outcome

addToHistory previously swallowed SQLite write failures with a console
warning while still pushing the entry into the in-memory history store
— a silent data-loss bug, since the row vanished on next restart with
no signal to the user. It now returns { persisted, error }, only mutates
in-memory history after the disk write lands, and the dictation flow
awaits the result so it can surface a "Couldn't save" toast and keep
the user's content intact for a retry. The new dictation autosave
writes transcript + segments to localStorage on a 1.5s debounce, on
every segment boundary, and on visibilitychange/pagehide — covering
both live capture and the stopped-but-unsaved walk-away case. On
DictationPage mount any draft <24h old surfaces an inline
Restore/Discard banner that styles as a peer to the existing error
notices. The draft is cleared only after a successful SQLite write or
explicit Discard/Clear; on persistence failure it stays on disk so the
user can recover on relaunch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 18:36:27 +01:00
parent 6389fc2ce7
commit e3292bd597
4 changed files with 359 additions and 9 deletions

View File

@@ -223,12 +223,31 @@ if (typeof window !== "undefined" && hasTauriRuntime()) {
loadHistory().catch(() => {});
}
export async function addToHistory(entry: TranscriptWriteEntry) {
const normalised = normaliseTranscriptEntry(entry);
history.unshift(normalised);
if (history.length > 500) history.length = 500;
/**
* Result of an `addToHistory` call. `persisted: true` means the entry is
* safely on disk (Tauri SQLite write succeeded, or we're in the non-Tauri
* shell where there is no disk). `persisted: false` means the in-memory
* history was NOT updated either — the caller is responsible for keeping
* the user's source content (e.g. the dictation draft) alive so a retry
* is possible. Surfacing this is what closes the historical silent-loss
* bug where SQLite failures were swallowed by `console.warn`.
*/
export interface AddToHistoryResult {
persisted: boolean;
error?: unknown;
}
if (!hasTauriRuntime()) return;
export async function addToHistory(entry: TranscriptWriteEntry): Promise<AddToHistoryResult> {
const normalised = normaliseTranscriptEntry(entry);
// Non-Tauri shell (browser preview, SSR test): no disk to fail. Update
// the in-memory store and report success so callers don't think the
// write was lost — there was no write to lose.
if (!hasTauriRuntime()) {
history.unshift(normalised);
if (history.length > 500) history.length = 500;
return { persisted: true };
}
try {
await invoke("add_transcript", {
@@ -251,14 +270,22 @@ export async function addToHistory(entry: TranscriptWriteEntry) {
antiHallucination: !!entry.antiHallucination,
},
});
// Only mutate the in-memory history after the disk write lands.
// Adding to memory while the disk write fails is exactly the misleading
// state PR 1.4 set out to fix — the row would appear in History, then
// silently vanish on next launch.
history.unshift(normalised);
if (history.length > 500) history.length = 500;
// Fire-and-forget auto-title. Same gate as the existing auto-cleanup
// path (`aiTier !== "off" && formatMode !== "Raw"`) so users opt in
// by the same Settings choice — no new toggle. Skipped silently if
// the LLM isn't loaded; HistoryPage's per-row "Title" button is the
// retry path. Never overwrites a title the caller already set.
void maybeAutoGenerateTitle(normalised.id, normalised.text, normalised.title);
return { persisted: true };
} catch (err) {
console.warn("addToHistory: SQLite write failed, entry will not persist past restart", err);
return { persisted: false, error: err };
}
}