Skip to content

Commit 4648bc9

Browse files
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.
1 parent e0851bc commit 4648bc9

File tree

2 files changed

+113
-2
lines changed

2 files changed

+113
-2
lines changed

apps/desktop/src/store/zustand/ai-task/task-configs/enhance-success.test.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
11
import type { LanguageModel } from "ai";
2-
import { describe, expect, it, vi } from "vitest";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
33

44
import type { TaskConfig } from ".";
55
import { enhanceSuccess } from "./enhance-success";
66

7+
const mocks = vi.hoisted(() => ({
8+
isFocused: vi.fn().mockResolvedValue(true),
9+
showNotification: vi.fn().mockResolvedValue({ status: "ok", data: null }),
10+
}));
11+
12+
vi.mock("@tauri-apps/api/window", () => ({
13+
getCurrentWindow: () => ({
14+
isFocused: mocks.isFocused,
15+
}),
16+
}));
17+
18+
vi.mock("@hypr/plugin-notification", () => ({
19+
commands: {
20+
showNotification: mocks.showNotification,
21+
},
22+
}));
23+
724
type EnhanceSuccessParams = Parameters<
825
NonNullable<TaskConfig<"enhance">["onSuccess"]>
926
>[0];
@@ -35,6 +52,13 @@ function createParams(
3552
}
3653

3754
describe("enhanceSuccess.onSuccess", () => {
55+
beforeEach(() => {
56+
mocks.isFocused.mockReset();
57+
mocks.isFocused.mockResolvedValue(true);
58+
mocks.showNotification.mockReset();
59+
mocks.showNotification.mockResolvedValue({ status: "ok", data: null });
60+
});
61+
3862
it("persists enhanced note content as TipTap JSON string", async () => {
3963
const params = createParams();
4064

@@ -98,4 +122,44 @@ describe("enhanceSuccess.onSuccess", () => {
98122

99123
expect(params.startTask).not.toHaveBeenCalled();
100124
});
125+
126+
it("shows a notification when summary generation finishes out of focus", async () => {
127+
mocks.isFocused.mockResolvedValue(false);
128+
const store = {
129+
setPartialRow: vi.fn(),
130+
getCell: vi.fn((table: string, _row: string, cell: string) => {
131+
if (table === "enhanced_notes" && cell === "title") {
132+
return "Summary";
133+
}
134+
if (table === "sessions" && cell === "title") {
135+
return "Weekly sync";
136+
}
137+
return "";
138+
}),
139+
} as unknown as EnhanceSuccessParams["store"];
140+
const params = createParams({ store });
141+
142+
await enhanceSuccess.onSuccess?.(params);
143+
144+
expect(mocks.showNotification).toHaveBeenCalledWith({
145+
key: null,
146+
title: "Summary ready",
147+
message: "Weekly sync",
148+
timeout: null,
149+
source: null,
150+
start_time: null,
151+
participants: null,
152+
event_details: null,
153+
action_label: null,
154+
options: null,
155+
});
156+
});
157+
158+
it("does not show a notification when the app is focused", async () => {
159+
const params = createParams();
160+
161+
await enhanceSuccess.onSuccess?.(params);
162+
163+
expect(mocks.showNotification).not.toHaveBeenCalled();
164+
});
101165
});

apps/desktop/src/store/zustand/ai-task/task-configs/enhance-success.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,53 @@
1+
import { getCurrentWindow } from "@tauri-apps/api/window";
2+
3+
import { commands as notificationCommands } from "@hypr/plugin-notification";
14
import { md2json } from "@hypr/tiptap/shared";
25

36
import { createTaskId, type TaskConfig } from ".";
47

5-
const onSuccess: NonNullable<TaskConfig<"enhance">["onSuccess"]> = ({
8+
async function maybeShowSummaryReadyNotification(
9+
store: Parameters<
10+
NonNullable<TaskConfig<"enhance">["onSuccess"]>
11+
>[0]["store"],
12+
args: Parameters<NonNullable<TaskConfig<"enhance">["onSuccess"]>>[0]["args"],
13+
) {
14+
try {
15+
const isFocused = await getCurrentWindow().isFocused();
16+
if (isFocused) {
17+
return;
18+
}
19+
} catch {
20+
return;
21+
}
22+
23+
const rawNoteTitle = store.getCell(
24+
"enhanced_notes",
25+
args.enhancedNoteId,
26+
"title",
27+
);
28+
const noteTitle =
29+
typeof rawNoteTitle === "string" && rawNoteTitle.trim()
30+
? rawNoteTitle.trim()
31+
: "Summary";
32+
const rawSessionTitle = store.getCell("sessions", args.sessionId, "title");
33+
const sessionTitle =
34+
typeof rawSessionTitle === "string" ? rawSessionTitle.trim() : "";
35+
36+
void notificationCommands.showNotification({
37+
key: null,
38+
title: `${noteTitle} ready`,
39+
message: sessionTitle || "Your meeting summary has been generated.",
40+
timeout: null,
41+
source: null,
42+
start_time: null,
43+
participants: null,
44+
event_details: null,
45+
action_label: null,
46+
options: null,
47+
});
48+
}
49+
50+
const onSuccess: NonNullable<TaskConfig<"enhance">["onSuccess"]> = async ({
651
text,
752
args,
853
model,
@@ -24,6 +69,8 @@ const onSuccess: NonNullable<TaskConfig<"enhance">["onSuccess"]> = ({
2469
return;
2570
}
2671

72+
await maybeShowSummaryReadyNotification(store, args);
73+
2774
const currentTitle = store.getCell("sessions", args.sessionId, "title");
2875
const trimmedTitle =
2976
typeof currentTitle === "string" ? currentTitle.trim() : "";

0 commit comments

Comments
 (0)