Configurable audio ping for room activity when the tab isn't focused - #532
Closed
juliarvalenti wants to merge 2 commits into
Closed
Configurable audio ping for room activity when the tab isn't focused#532juliarvalenti wants to merge 2 commits into
juliarvalenti wants to merge 2 commits into
Conversation
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.
This was referenced Aug 15, 2026
Closed
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>
Contributor
Author
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.
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 sharedAudioContextfrom a user gesture, satisfying the browser's autoplay-gesture requirement.src/components/notification-settings.tsx— locally persisted preferences (on/off, scope, sound, volume) underlocalStorage["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 SSEonmessagehandler now checksdocument.hiddenand the current scope before playing a tone:event-stream.*.test.tsxfiles now render through a smallrenderWithProviderstest helper (src/test/render-with-providers.tsx) sinceEventStreamnow depends on the acting-as/notification-settings contexts.event-stream.audio-ping.test.tsxcovers: 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
pnpm test— 19 passed, including the 7 new audio-ping tests)pnpm lint/tsc --noEmit)pnpm build)MYCELIUM_UI_MOCK=1dev 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}tolocalStoragewith no console errors from the new codeRelated Issues
Closes #514
Generated by Claude Code