feat(notifications): keep background tabs alive + favicon on dock/banner (t066)#4
Open
nguyenthienthanh wants to merge 1 commit into
Open
Conversation
Author
|
@duongdev — assigning you as reviewer (couldn't set the assignee field from a fork PR without write access). This fixes background tabs going silent (Chromium freezes idle tabs → page-script notification hook stops). t067 (service-worker push capture) follows as a separate PR. |
duongdev
approved these changes
Jun 8, 2026
8c9e466 to
e4ebc99
Compare
nguyenthienthanh
added a commit
to nguyenthienthanh/cdp-browser
that referenced
this pull request
Jun 8, 2026
…ack (t067) Slack delivers many notifications from its service worker's push handler via registration.showNotification — a realm the page hook (window.Notification) can't reach, so they were silently missed. - The side-channel now also attaches to a matching service_worker target (Slack adapter swScript) and injects inject/slack-sw-notify.js via Runtime.evaluate (a worker has no Page domain, so no document-start hook and no t066 keep-alive), patching ServiceWorkerRegistration.prototype.showNotification to ship the same __cdpNotify toasts. - The single Slack SW serves every workspace (origin-level), so the SW URL has no team id; the script derives the per-workspace groupKey from the notification payload (defensive probe, logged once for HITL). Known gap: a worker that spins up fresh on a push and fires before the next 5s reconcile attaches is missed (no SW-start barrier). Documented in docs/tasks/067; a hardened version would use a browser-level Target.setAutoAttach waitForDebuggerOnStart. Stacked on t066 (duongdev#4) — merge that first.
…ner (t066) Background tabs on the remote browser silently stopped delivering notifications: Chromium freezes idle background tabs (~5 min), which pauses the page JS the capture script hooks (window.Notification), so only the active tab kept notifying. - Keep-alive: the side-channel now sends Page.setWebLifecycleState active on attach and re-applies it every reconcile. This un-freezes the tab without making it visible (per the CDP spec the state only governs freeze, not document.visibilityState), so Slack still treats the tab as hidden and keeps firing desktop notifications. Lives in core/ so the headless web server benefits too. - Favicon: the OS notification banner and the macOS dock icon now carry the source app's favicon (newest-unread; cleared when unread -> 0). dockOverlayIcon(list) is pure; main fetches the favicon bytes (no CORS wall) and composites in the renderer (decodes .ico, no canvas taint). Service-worker push capture (registration.showNotification, a separate realm) is out of scope here -> t067.
e4ebc99 to
88b6fd7
Compare
nguyenthienthanh
added a commit
to nguyenthienthanh/cdp-browser
that referenced
this pull request
Jun 8, 2026
…ack (t067) Slack delivers many notifications from its service worker's push handler via registration.showNotification — a realm the page hook (window.Notification) can't reach, so they were silently missed. - The side-channel now also attaches to a matching service_worker target (Slack adapter swScript) and injects inject/slack-sw-notify.js via Runtime.evaluate (a worker has no Page domain, so no document-start hook and no t066 keep-alive), patching ServiceWorkerRegistration.prototype.showNotification to ship the same __cdpNotify toasts. - The single Slack SW serves every workspace (origin-level), so the SW URL has no team id; the script derives the per-workspace groupKey from the notification payload (defensive probe, logged once for HITL). Known gap: a worker that spins up fresh on a push and fires before the next 5s reconcile attaches is missed (no SW-start barrier). Documented in docs/tasks/067; a hardened version would use a browser-level Target.setAutoAttach waitForDebuggerOnStart. Stacked on t066 (duongdev#4) — merge that first.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Notifications only reached the real machine for the active remote tab — background tabs (e.g. a second Slack workspace) went silent after a few minutes.
Root cause: Chromium freezes idle background tabs (~5 min), pausing the page JS that our capture script hooks (
window.Notification). A frozen tab stops callingnew Notification(), so the side-channel receives nothing. Only the active tab kept its JS running, hence only it notified.Changes
A — Keep notification tabs alive
Page.setWebLifecycleState({state:"active"})on attach and re-applies it everyreconcile(the browser can re-freeze).setWebLifecycleStateaccepts only"frozen"|"active"and governs freeze state, notdocument.visibilityState. So the tab un-freezes while staying hidden → Slack keeps treating it as background and keeps firing desktop notifications for us to capture.core/notifications-sidechain.js, so the headless web server benefits too.B — Favicon on notification banner + macOS dock icon
dockOverlayIcon(list)is pure/tested. main fetches the favicon bytes (no browser CORS wall) and composites in the chrome renderer (its<img>decodes.ico; data-URL inputs never taint the canvas), thenapp.dock.setIcon+Notification({ icon }).Out of scope → follow-up PR (t067)
Service-worker
push-handler notifications (registration.showNotification) run in a separate JS realm the page hook can't reach. Capturing those needs a new attach path against theservice_workertarget and live HITL testing — shipping separately.Tests
pnpm test— 629 passing (new: keep-alive sequence +dockOverlayIcon).pnpm typecheck, Biome (changed files) — clean.pnpm install:local; pending manual smoke against a live remote browser (background a Slack tab > 5 min → OS toast fires; favicon shows on banner + dock; mark-all-read clears the dock badge).🤖 Generated with Claude Code