Skip to content

Commit ce0e3b7

Browse files
fix(stt): stabilize batch completion notifications
Use unique batch completion notification keys and recover session IDs from notification keys when sources are missing.
1 parent 84d2523 commit ce0e3b7

5 files changed

Lines changed: 89 additions & 5 deletions

File tree

apps/desktop/src/services/event-listeners.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
44
import { EventListeners } from "./event-listeners";
55

66
import { createAutoStopEndedNotificationKey } from "~/stt/auto-stop-notification";
7+
import { createBatchCompletedNotificationKey } from "~/stt/batch-completed-notification";
78

89
const {
910
notificationListenMock,
@@ -233,6 +234,34 @@ describe("EventListeners notification events", () => {
233234
});
234235
});
235236

237+
test("notification_confirm with batch key opens that session without source", async () => {
238+
useMainStoreMock.mockReturnValue({} as never);
239+
240+
render(<EventListeners />);
241+
242+
await vi.waitFor(() =>
243+
expect(notificationListenMock).toHaveBeenCalledTimes(1),
244+
);
245+
246+
const handler = notificationListenMock.mock.calls[0]?.[0];
247+
expect(handler).toBeTypeOf("function");
248+
249+
handler({
250+
payload: {
251+
type: "notification_confirm",
252+
key: createBatchCompletedNotificationKey("session-1"),
253+
source: null,
254+
},
255+
});
256+
257+
expect(createSessionMock).not.toHaveBeenCalled();
258+
expect(openNewMock).toHaveBeenCalledWith({
259+
type: "sessions",
260+
id: "session-1",
261+
state: { view: null, autoStart: null },
262+
});
263+
});
264+
236265
test("notification_confirm with mic_detected source sets triggerAppIds (regression: #5120 confirm path)", async () => {
237266
useMainStoreMock.mockReturnValue({} as never);
238267

apps/desktop/src/services/event-listeners.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import * as settings from "~/store/tinybase/store/settings";
1717
import { listenerStore } from "~/store/zustand/listener/instance";
1818
import { useTabs } from "~/store/zustand/tabs";
1919
import { parseAutoStopEndedNotificationKey } from "~/stt/auto-stop-notification";
20+
import { parseBatchCompletedNotificationKey } from "~/stt/batch-completed-notification";
2021

2122
type MainStore = NonNullable<ReturnType<typeof main.UI.useStore>>;
2223

@@ -179,7 +180,7 @@ function useNotificationEvents() {
179180
const sourceSessionId =
180181
payload.source?.type === "session"
181182
? payload.source.session_id
182-
: null;
183+
: parseBatchCompletedNotificationKey(payload.key);
183184
const triggerAppIds =
184185
payload.source?.type === "mic_detected"
185186
? (payload.source.app_ids ?? null)

apps/desktop/src/store/zustand/listener/general-batch.test.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import { beforeEach, describe, expect, test, vi } from "vitest";
22

33
import { EMPTY_BATCH_TRANSCRIPT_ERROR } from "./batch";
4-
import { runBatchSession } from "./general-batch";
4+
import {
5+
runBatchSession,
6+
showBatchCompletedNotification,
7+
} from "./general-batch";
8+
9+
import { parseBatchCompletedNotificationKey } from "~/stt/batch-completed-notification";
510

611
const {
712
isFocusedMock,
@@ -217,15 +222,30 @@ describe("runBatchSession", () => {
217222
},
218223
);
219224

220-
expect(showNotificationMock).toHaveBeenCalledWith(
225+
const notification = showNotificationMock.mock.calls[0]?.[0];
226+
expect(notification).toEqual(
221227
expect.objectContaining({
222-
key: "batch-completed-session-1",
223228
title: "Transcription complete",
224229
message: "Your transcript is ready.",
225230
action_label: "Open Anarlog",
226231
source: { type: "session", session_id: "session-1" },
227232
}),
228233
);
234+
expect(parseBatchCompletedNotificationKey(notification.key)).toBe(
235+
"session-1",
236+
);
237+
});
238+
239+
test("uses a fresh notification key for each batch completion", async () => {
240+
await showBatchCompletedNotification("session-1", { force: true });
241+
await showBatchCompletedNotification("session-1", { force: true });
242+
243+
const firstKey = showNotificationMock.mock.calls[0]?.[0].key;
244+
const secondKey = showNotificationMock.mock.calls[1]?.[0].key;
245+
246+
expect(parseBatchCompletedNotificationKey(firstKey)).toBe("session-1");
247+
expect(parseBatchCompletedNotificationKey(secondKey)).toBe("session-1");
248+
expect(firstKey).not.toBe(secondKey);
229249
});
230250

231251
test("forwards streamed progress events before completion", async () => {

apps/desktop/src/store/zustand/listener/general-batch.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
type BatchState,
1616
} from "./batch";
1717

18+
import { createBatchCompletedNotificationKey } from "~/stt/batch-completed-notification";
19+
1820
type BatchStore = BatchActions & BatchState;
1921

2022
async function shouldNotifyBatchCompleted() {
@@ -42,7 +44,7 @@ export async function showBatchCompletedNotification(
4244

4345
try {
4446
const result = await notificationCommands.showNotification({
45-
key: `batch-completed-${sessionId}`,
47+
key: createBatchCompletedNotificationKey(sessionId),
4648
title: "Transcription complete",
4749
message: "Your transcript is ready.",
4850
timeout: null,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const LEGACY_BATCH_COMPLETED_NOTIFICATION_KEY_PREFIX = "batch-completed-";
2+
3+
export const BATCH_COMPLETED_NOTIFICATION_KEY_PREFIX =
4+
"batch-completed:" as const;
5+
6+
export function createBatchCompletedNotificationKey(sessionId: string) {
7+
return `${BATCH_COMPLETED_NOTIFICATION_KEY_PREFIX}${sessionId}:${crypto.randomUUID()}`;
8+
}
9+
10+
export function parseBatchCompletedNotificationKey(
11+
key: string | null | undefined,
12+
) {
13+
if (!key) {
14+
return null;
15+
}
16+
17+
if (key.startsWith(BATCH_COMPLETED_NOTIFICATION_KEY_PREFIX)) {
18+
const value = key.slice(BATCH_COMPLETED_NOTIFICATION_KEY_PREFIX.length);
19+
const separatorIndex = value.lastIndexOf(":");
20+
return (
21+
value.slice(0, separatorIndex === -1 ? undefined : separatorIndex) || null
22+
);
23+
}
24+
25+
if (key.startsWith(LEGACY_BATCH_COMPLETED_NOTIFICATION_KEY_PREFIX)) {
26+
return (
27+
key.slice(LEGACY_BATCH_COMPLETED_NOTIFICATION_KEY_PREFIX.length) || null
28+
);
29+
}
30+
31+
return null;
32+
}

0 commit comments

Comments
 (0)