Skip to content

Configurable audio ping for room activity when the tab isn't focused - #532

Closed
juliarvalenti wants to merge 2 commits into
mainfrom
claude/github-issue-514-onhh1b
Closed

Configurable audio ping for room activity when the tab isn't focused#532
juliarvalenti wants to merge 2 commits into
mainfrom
claude/github-issue-514-onhh1b

Conversation

@juliarvalenti

Copy link
Copy Markdown
Contributor

Summary

Adds an audio ping for room activity while the browser tab is hidden/unfocused, so a reply, mention, or consensus doesn't require keeping the tab visible to notice. Sourced entirely off the existing SSE stream in event-stream.tsx — no backend changes.

Changes

  • src/lib/audio-ping.ts — synthesizes three short tones (chime/ping/tone) via Web Audio oscillators, no audio assets to ship. primeAudio() creates/resumes the shared AudioContext from a user gesture, satisfying the browser's autoplay-gesture requirement.
  • src/components/notification-settings.tsx — locally persisted preferences (on/off, scope, sound, volume) under localStorage["mycelium.notify"], mirroring the existing acting-as identity pattern (current-user.tsx). Wired into the root layout as a context provider.
  • src/components/notification-bell.tsx — a bell popover in the room header (next to the acting-as picker) to toggle pinging, pick scope, sound, and volume, with a click-to-preview on each sound option.
  • event-stream.tsx — the SSE onmessage handler now checks document.hidden and the current scope before playing a tone:
    • Scope "Needs me" (default, narrow): consensus always qualifies; chat only qualifies when it's directed at me, directed at an agent I own, or @-mentions either — never for my own outgoing messages.
    • Scope "Everything": any activity visible in the channel view.
    • Rate-limited to one ping per 4s so a burst of messages/negotiation ticks doesn't turn into a machine-gun.
  • Existing event-stream.*.test.tsx files now render through a small renderWithProviders test helper (src/test/render-with-providers.tsx) since EventStream now depends on the acting-as/notification-settings contexts.
  • New event-stream.audio-ping.test.tsx covers: no ping while visible, no ping when disabled, scope "all" pings on a broadcast, scope "needs-me" ignores unrelated chat and pings on an @-mention, scope "needs-me" always pings on consensus, and a burst of activity debounces to a single ping.

Testing

  • Unit tests pass (pnpm test — 19 passed, including the 7 new audio-ping tests)
  • Type check passes (pnpm lint / tsc --noEmit)
  • Production build passes (pnpm build)
  • Manual check in MYCELIUM_UI_MOCK=1 dev mode via headless Chromium: bell renders in the room header, popover opens with the enable toggle/scope/sound/volume controls, toggling on persists {"enabled":true,"scope":"needs-me","sound":"ping","volume":0.8} to localStorage with no console errors from the new code

Related Issues

Closes #514


Generated by Claude Code

claude added 2 commits August 14, 2026 22:45
Add an audio ping (issue #514) for room activity while the tab is
hidden/unfocused, sourced off the existing SSE stream in
event-stream.tsx with no backend changes.

- src/lib/audio-ping.ts: synthesizes short tones via Web Audio
  oscillators (no audio assets), with a primeAudio() unlock for the
  browser's user-gesture requirement.
- src/components/notification-settings.tsx: locally persisted prefs
  (on/off, scope, sound, volume), mirroring the acting-as identity's
  localStorage pattern.
- src/components/notification-bell.tsx: room-header popover control.
- event-stream.tsx: pings on relevant SSE events only when
  document.hidden, rate-limited so a burst of messages is one ping.
  Scope "needs me" keys off the acting-as principal + owned-agent
  mentions/recipients; consensus always qualifies.
next dev (Next.js 16) writes AGENTS.md/CLAUDE.md into the project on
every run and re-adds them if removed; committing keeps the tree
clean instead of leaving them perpetually untracked.
juliarvalenti added a commit that referenced this pull request Aug 15, 2026
* feat(notifications): cross-room, cross-tab notification center

Replaces the never-merged per-room audio ping (#532) with a real
notifications system, as scoped in #536.

Backend:
- New GET /notifications/stream aggregates every room's activity (plus
  room create/delete) into one SSE connection, so the client doesn't have to
  open a per-room stream for every room the user participates in. It follows
  rooms created/deleted after connecting by watching the same app-channel
  frames the sidebar already consumes.
- Fix the real classification bug flagged in #536: parseEvent only unwrapped
  l9_exchange, so live consensus (l9_commit) and knowledge (l9_knowledge)
  fell through to the unhandled-type console.warn fallback. Both now unwrap
  correctly — consensus renders as a real "Consensus"/"Timeout" notice (and
  feeds NegotiationView's convergence state) instead of never firing.

Frontend:
- lib/notifications.ts: classifies a wire frame into mention/direct/
  consensus/knowledge/join, scoped to the acting-as principal; a settings
  predicate (scope, per-room mute, global DND); localStorage persistence;
  BroadcastChannel cross-tab sync for read/dismiss/clear/settings; Web
  Locks-based leader election so only one open tab pings/desktop-notifies.
- lib/audio-ping.ts: dependency-free synthesized chime + gesture unlock.
- components/notifications-provider.tsx: root-mounted provider owning the
  single global SSE subscription, unread state, and the tab-title badge.
- components/notification-bell.tsx + notification-settings.tsx: the header
  bell/inbox (mark read, dismiss, mute room, clear all) and a settings
  dialog (scope, sound + volume, desktop notifications, DND, muted rooms).

Deliberately client-side only for now (per #536's "or purely client-side to
start") — no persisted server-side notification ledger.

* fix(notifications): gate the ping on window focus, not tab visibility

The audio ping / desktop notification gated on `document.visibilityState`,
which only flips to hidden when you switch tabs or minimize — a window merely
behind another app stays "visible", so alt-tabbing to another app silently
dropped every ping. Gate on `!document.hasFocus()` instead: false when the tab
is backgrounded, minimized, OR the browser isn't the focused app — matching the
"notify me when I'm not looking at this" intent.

---------

Co-authored-by: Claude <noreply@anthropic.com>
@juliarvalenti

Copy link
Copy Markdown
Contributor Author

Superseded by #537 (the full notifications system per #536). The per-room ping design is replaced by a cross-room aggregate stream + inbox, so closing without merging.

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.

Configurable audio ping for room activity when the tab isn't focused

2 participants