From eb1d194447fb7585f3600b043d912b45a9c45545 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 6 Mar 2026 13:46:25 +0000 Subject: [PATCH] fix(site): send web push notification when browser is unfocused on agent page (#22710) --- site/src/serviceWorker.test.ts | 58 +++++++++++++++++++++++++++++++--- site/src/serviceWorker.ts | 4 ++- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/site/src/serviceWorker.test.ts b/site/src/serviceWorker.test.ts index dedd2bef1a..a3f432eb60 100644 --- a/site/src/serviceWorker.test.ts +++ b/site/src/serviceWorker.test.ts @@ -7,7 +7,11 @@ import type { WebpushMessage } from "api/typesGenerated"; const mockShowNotification = vi.fn(() => Promise.resolve()); const mockRegistration = { showNotification: mockShowNotification }; const mockMatchAll = - vi.fn<() => Promise>>(); + vi.fn< + () => Promise< + Array<{ visibilityState: string; url: string; focused: boolean }> + > + >(); const mockClients = { matchAll: mockMatchAll, claim: vi.fn(() => Promise.resolve()), @@ -81,12 +85,17 @@ describe("serviceWorker push handler", () => { body: testPayload.body, icon: testPayload.icon, data: testPayload.data, + tag: undefined, }); }); it("suppresses notification when viewing the specific chat", async () => { mockMatchAll.mockResolvedValue([ - { visibilityState: "visible", url: "https://example.com/agents/abc" }, + { + visibilityState: "visible", + url: "https://example.com/agents/abc", + focused: true, + }, ]); const event = makePushEvent(testPayload); @@ -101,6 +110,7 @@ describe("serviceWorker push handler", () => { { visibilityState: "visible", url: "https://example.com/agents/other-chat-id", + focused: true, }, ]); @@ -112,12 +122,17 @@ describe("serviceWorker push handler", () => { body: testPayload.body, icon: testPayload.icon, data: testPayload.data, + tag: undefined, }); }); it("shows notification when payload has no data url", async () => { mockMatchAll.mockResolvedValue([ - { visibilityState: "visible", url: "https://example.com/agents/abc" }, + { + visibilityState: "visible", + url: "https://example.com/agents/abc", + focused: true, + }, ]); const payload: WebpushMessage = { @@ -134,12 +149,17 @@ describe("serviceWorker push handler", () => { body: "test", icon: "/icon.png", data: undefined, + tag: undefined, }); }); it("shows notification when specific chat page exists but is hidden", async () => { mockMatchAll.mockResolvedValue([ - { visibilityState: "hidden", url: "https://example.com/agents/abc" }, + { + visibilityState: "hidden", + url: "https://example.com/agents/abc", + focused: false, + }, ]); const event = makePushEvent(testPayload); @@ -150,12 +170,38 @@ describe("serviceWorker push handler", () => { body: testPayload.body, icon: testPayload.icon, data: testPayload.data, + tag: undefined, + }); + }); + + it("shows notification when viewing the specific chat but browser is not focused", async () => { + mockMatchAll.mockResolvedValue([ + { + visibilityState: "visible", + url: "https://example.com/agents/abc", + focused: false, + }, + ]); + + const event = makePushEvent(testPayload); + handlers.push(event); + await event._waitUntilPromise; + + expect(mockShowNotification).toHaveBeenCalledWith(testPayload.title, { + body: testPayload.body, + icon: testPayload.icon, + data: testPayload.data, + tag: undefined, }); }); it("shows notification when visible window is not on agents page", async () => { mockMatchAll.mockResolvedValue([ - { visibilityState: "visible", url: "https://example.com/settings" }, + { + visibilityState: "visible", + url: "https://example.com/settings", + focused: true, + }, ]); const event = makePushEvent(testPayload); @@ -166,6 +212,7 @@ describe("serviceWorker push handler", () => { body: testPayload.body, icon: testPayload.icon, data: testPayload.data, + tag: undefined, }); }); @@ -194,6 +241,7 @@ describe("serviceWorker push handler", () => { body: "", icon: "/favicon.ico", data: undefined, + tag: undefined, }); }); }); diff --git a/site/src/serviceWorker.ts b/site/src/serviceWorker.ts index 5bd1e14539..1a8d73a9e4 100644 --- a/site/src/serviceWorker.ts +++ b/site/src/serviceWorker.ts @@ -30,12 +30,14 @@ self.addEventListener("push", (event) => { .matchAll({ type: "window", includeUncontrolled: true }) .then((clientList) => { // Only suppress if the user is actively viewing the - // specific chat that triggered this notification. + // specific chat that triggered this notification and + // the browser window is focused. const chatURL = payload.data?.url; if (chatURL) { const isVisible = clientList.some( (client) => client.visibilityState === "visible" && + client.focused && client.url.includes(chatURL), ); if (isVisible) {