Skip to content

feat(notifications): keep background tabs alive + favicon on dock/banner (t066)#4

Open
nguyenthienthanh wants to merge 1 commit into
duongdev:mainfrom
nguyenthienthanh:feat/t066-keep-notif-tabs-alive
Open

feat(notifications): keep background tabs alive + favicon on dock/banner (t066)#4
nguyenthienthanh wants to merge 1 commit into
duongdev:mainfrom
nguyenthienthanh:feat/t066-keep-notif-tabs-alive

Conversation

@nguyenthienthanh

Copy link
Copy Markdown

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 calling new 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

  • The notifications side-channel now sends Page.setWebLifecycleState({state:"active"}) on attach and re-applies it every reconcile (the browser can re-freeze).
  • Verified against the CDP protocol: setWebLifecycleState accepts only "frozen"|"active" and governs freeze state, not document.visibilityState. So the tab un-freezes while staying hidden → Slack keeps treating it as background and keeps firing desktop notifications for us to capture.
  • Lives in core/notifications-sidechain.js, so the headless web server benefits too.

B — Favicon on notification banner + macOS dock icon

  • The OS notification banner and the dock icon now carry the source app's favicon (newest-unread app; cleared when unread → 0), so you can tell which app pinged you.
  • 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), then app.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 the service_worker target and live HITL testing — shipping separately.

Tests

  • pnpm test629 passing (new: keep-alive sequence + dockOverlayIcon).
  • pnpm typecheck, Biome (changed files) — clean.
  • Built + installed via 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

@nguyenthienthanh

Copy link
Copy Markdown
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.

@nguyenthienthanh nguyenthienthanh force-pushed the feat/t066-keep-notif-tabs-alive branch from 8c9e466 to e4ebc99 Compare June 8, 2026 09:46
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.
@nguyenthienthanh nguyenthienthanh force-pushed the feat/t066-keep-notif-tabs-alive branch from e4ebc99 to 88b6fd7 Compare June 8, 2026 09:54
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants