From 5d9b1f7a9fdf8c5934aebcd80c0b57af6202c334 Mon Sep 17 00:00:00 2001 From: ComputelessComputer Date: Mon, 30 Mar 2026 13:41:36 -0700 Subject: [PATCH] fix: notify when summaries finish in background Show a persistent desktop notification after summary generation completes while the app window is out of focus, with tests covering focused and unfocused behavior. --- .../task-configs/enhance-success.test.ts | 66 ++++++++++++++++++- .../ai-task/task-configs/enhance-success.ts | 49 +++++++++++++- 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/store/zustand/ai-task/task-configs/enhance-success.test.ts b/apps/desktop/src/store/zustand/ai-task/task-configs/enhance-success.test.ts index a409d62dad..ef59519c9d 100644 --- a/apps/desktop/src/store/zustand/ai-task/task-configs/enhance-success.test.ts +++ b/apps/desktop/src/store/zustand/ai-task/task-configs/enhance-success.test.ts @@ -1,9 +1,26 @@ import type { LanguageModel } from "ai"; -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { TaskConfig } from "."; import { enhanceSuccess } from "./enhance-success"; +const mocks = vi.hoisted(() => ({ + isFocused: vi.fn().mockResolvedValue(true), + showNotification: vi.fn().mockResolvedValue({ status: "ok", data: null }), +})); + +vi.mock("@tauri-apps/api/window", () => ({ + getCurrentWindow: () => ({ + isFocused: mocks.isFocused, + }), +})); + +vi.mock("@hypr/plugin-notification", () => ({ + commands: { + showNotification: mocks.showNotification, + }, +})); + type EnhanceSuccessParams = Parameters< NonNullable["onSuccess"]> >[0]; @@ -35,6 +52,13 @@ function createParams( } describe("enhanceSuccess.onSuccess", () => { + beforeEach(() => { + mocks.isFocused.mockReset(); + mocks.isFocused.mockResolvedValue(true); + mocks.showNotification.mockReset(); + mocks.showNotification.mockResolvedValue({ status: "ok", data: null }); + }); + it("persists enhanced note content as TipTap JSON string", async () => { const params = createParams(); @@ -98,4 +122,44 @@ describe("enhanceSuccess.onSuccess", () => { expect(params.startTask).not.toHaveBeenCalled(); }); + + it("shows a notification when summary generation finishes out of focus", async () => { + mocks.isFocused.mockResolvedValue(false); + const store = { + setPartialRow: vi.fn(), + getCell: vi.fn((table: string, _row: string, cell: string) => { + if (table === "enhanced_notes" && cell === "title") { + return "Summary"; + } + if (table === "sessions" && cell === "title") { + return "Weekly sync"; + } + return ""; + }), + } as unknown as EnhanceSuccessParams["store"]; + const params = createParams({ store }); + + await enhanceSuccess.onSuccess?.(params); + + expect(mocks.showNotification).toHaveBeenCalledWith({ + key: null, + title: "Summary ready", + message: "Weekly sync", + timeout: null, + source: null, + start_time: null, + participants: null, + event_details: null, + action_label: null, + options: null, + }); + }); + + it("does not show a notification when the app is focused", async () => { + const params = createParams(); + + await enhanceSuccess.onSuccess?.(params); + + expect(mocks.showNotification).not.toHaveBeenCalled(); + }); }); diff --git a/apps/desktop/src/store/zustand/ai-task/task-configs/enhance-success.ts b/apps/desktop/src/store/zustand/ai-task/task-configs/enhance-success.ts index 916454c661..64dba5ff68 100644 --- a/apps/desktop/src/store/zustand/ai-task/task-configs/enhance-success.ts +++ b/apps/desktop/src/store/zustand/ai-task/task-configs/enhance-success.ts @@ -1,8 +1,53 @@ +import { getCurrentWindow } from "@tauri-apps/api/window"; + +import { commands as notificationCommands } from "@hypr/plugin-notification"; import { md2json } from "@hypr/tiptap/shared"; import { createTaskId, type TaskConfig } from "."; -const onSuccess: NonNullable["onSuccess"]> = ({ +async function maybeShowSummaryReadyNotification( + store: Parameters< + NonNullable["onSuccess"]> + >[0]["store"], + args: Parameters["onSuccess"]>>[0]["args"], +) { + try { + const isFocused = await getCurrentWindow().isFocused(); + if (isFocused) { + return; + } + } catch { + return; + } + + const rawNoteTitle = store.getCell( + "enhanced_notes", + args.enhancedNoteId, + "title", + ); + const noteTitle = + typeof rawNoteTitle === "string" && rawNoteTitle.trim() + ? rawNoteTitle.trim() + : "Summary"; + const rawSessionTitle = store.getCell("sessions", args.sessionId, "title"); + const sessionTitle = + typeof rawSessionTitle === "string" ? rawSessionTitle.trim() : ""; + + void notificationCommands.showNotification({ + key: null, + title: `${noteTitle} ready`, + message: sessionTitle || "Your meeting summary has been generated.", + timeout: null, + source: null, + start_time: null, + participants: null, + event_details: null, + action_label: null, + options: null, + }); +} + +const onSuccess: NonNullable["onSuccess"]> = async ({ text, args, model, @@ -24,6 +69,8 @@ const onSuccess: NonNullable["onSuccess"]> = ({ return; } + await maybeShowSummaryReadyNotification(store, args); + const currentTitle = store.getCell("sessions", args.sessionId, "title"); const trimmedTitle = typeof currentTitle === "string" ? currentTitle.trim() : "";