diff --git a/app/src/renderer/src/store.test.ts b/app/src/renderer/src/store.test.ts index 7675e96..a3bebdd 100644 --- a/app/src/renderer/src/store.test.ts +++ b/app/src/renderer/src/store.test.ts @@ -255,6 +255,34 @@ describe("submitCreate (session orchestration)", () => { expect(assistant.items.at(-1)).toMatchObject({ type: "status", icon: "error", label: "no clips" }); }); + it("a rejecting session:save (e.g. stale main without the handler) degrades to a log line", async () => { + vi.mocked(window.api.generateProject).mockResolvedValue({ ok: true }); + vi.mocked(window.api.saveSession).mockRejectedValue( + new Error("No handler registered for 'session:save'"), + ); + useEditor.setState({ + edl, + slug: "demo", + view: "editor", + session: { version: 1, turns: [] }, + sessionBusy: false, + notices: [], + reloadProject: () => {}, + }); + + await useEditor.getState().submitCreate({ text: "go", settings }); + // Let the fire-and-forget persistence promises settle. + await new Promise((r) => setTimeout(r, 0)); + + // The run itself completed, nothing toasted, and the failure was logged. + expect(useEditor.getState().session?.turns).toHaveLength(2); + expect(useEditor.getState().notices).toHaveLength(0); + expect(window.api.logRenderer).toHaveBeenCalledWith( + "warn", + expect.stringContaining("session save failed"), + ); + }); + it("applyCritiqueFixes marks the card applied and logs the autotune turn", async () => { vi.mocked(window.api.autoTune).mockResolvedValue({ ok: true }); vi.mocked(window.api.saveSession).mockResolvedValue({ ok: true }); diff --git a/app/src/renderer/src/store.ts b/app/src/renderer/src/store.ts index 7c00ea3..98a362c 100644 --- a/app/src/renderer/src/store.ts +++ b/app/src/renderer/src/store.ts @@ -177,6 +177,25 @@ export interface ExportResult { error?: string; } +/** + * Best-effort session persistence. The in-memory log is authoritative while a + * run streams; a failed write (disk error, or a stale main process without the + * session:save handler after an update) must degrade to a log line — not an + * unhandled rejection that toasts "Something failed" at the user mid-run. + */ +function persistSession(slug: string, session: Session): void { + void window.api + ?.saveSession(slug, session) + .then((res) => { + if (res && res.ok === false) { + void window.api?.logRenderer("warn", `session save failed: ${res.error ?? "unknown error"}`); + } + }) + .catch((err) => { + void window.api?.logRenderer("warn", `session save failed: ${String(err)}`); + }); +} + interface EditorState { view: View; projects: ProjectSummary[]; @@ -470,7 +489,7 @@ export const useEditor = create()((set, get) => ({ }); session = appendAssistantTurn(session, mode); set({ session, sessionBusy: true, ...(mode === "generation" ? { generating: true } : {}) }); - void window.api.saveSession(slug, session); + persistSession(slug, session); const prefix = mode === "generation" ? "generate" : "critique"; const offPhase = window.api.onPhase(prefix, (phase) => { @@ -534,12 +553,12 @@ export const useEditor = create()((set, get) => ({ s = completeAssistantTurn(s, res.error ?? "The run failed."); } set({ session: s }); - void window.api.saveSession(slug, s); + persistSession(slug, s); } catch (err) { if (get().slug === slug) { const s = completeAssistantTurn(get().session ?? emptySession(), String(err)); set({ session: s }); - void window.api.saveSession(slug, s); + persistSession(slug, s); } } finally { offPhase(); @@ -560,7 +579,7 @@ export const useEditor = create()((set, get) => ({ label: "applying the suggested fixes", }); set({ session, sessionBusy: true, autotuning: true }); - void window.api.saveSession(slug, session); + persistSession(slug, session); const offPhase = window.api.onPhase("autotune", (phase) => { if (get().slug !== slug) return; @@ -597,12 +616,12 @@ export const useEditor = create()((set, get) => ({ s = completeAssistantTurn(s, res.error ?? "Couldn't apply the fixes."); } set({ session: s }); - void window.api.saveSession(slug, s); + persistSession(slug, s); } catch (err) { if (get().slug === slug) { const s = completeAssistantTurn(get().session ?? emptySession(), String(err)); set({ session: s }); - void window.api.saveSession(slug, s); + persistSession(slug, s); } } finally { offPhase();