@@ -30,6 +30,9 @@ import {
3030// noticing within a minute, and the count is one indexed query.
3131const UNREAD_POLL_INTERVAL_MS = 60_000 ;
3232
33+ /** Wake ids already toasted, across reloads — see the dedupe note in the poll. */
34+ const TOASTED_WAKES_STORAGE_KEY = "tdev:dashboard-agent:toasted-wakes" ;
35+
3336/**
3437 * Mounts the dashboard agent in the env layout. Renders the page content
3538 * (`children` = the route Outlet) and shares the open/close state via context so
@@ -64,6 +67,31 @@ export function DashboardAgent({
6467 // wake the user has already been shown (and maybe dismissed) must not come
6568 // back every 60s while the chat stays unread.
6669 const toastedWakes = useRef ( new Set < string > ( ) ) ;
70+ // The poll's toast source is "recent deliveries", not "unread" — so the dedupe
71+ // must survive a reload, or every wake younger than the window re-toasts on
72+ // every refresh. Persisted per watch id; the recency window caps growth, and a
73+ // failed read (private mode) degrades to the in-memory set.
74+ useEffect ( ( ) => {
75+ try {
76+ const raw = window . localStorage . getItem ( TOASTED_WAKES_STORAGE_KEY ) ;
77+ if ( raw ) for ( const id of JSON . parse ( raw ) as string [ ] ) toastedWakes . current . add ( id ) ;
78+ } catch {
79+ // Storage unavailable — session-scoped dedupe still applies.
80+ }
81+ } , [ ] ) ;
82+ const rememberToasted = useCallback ( ( watchId : string ) => {
83+ toastedWakes . current . add ( watchId ) ;
84+ try {
85+ // Keep the newest ids only; the toast source window is 15 minutes, so a
86+ // small tail is plenty and the key can't grow unbounded.
87+ window . localStorage . setItem (
88+ TOASTED_WAKES_STORAGE_KEY ,
89+ JSON . stringify ( [ ...toastedWakes . current ] . slice ( - 50 ) )
90+ ) ;
91+ } catch {
92+ // Same degradation as the read.
93+ }
94+ } , [ ] ) ;
6795 // The chat currently on screen (panel open). A wake there streams into the
6896 // visible transcript, so it toasts but must not light the dot.
6997 const visibleChat = useRef < string | null > ( null ) ;
@@ -175,7 +203,7 @@ export function DashboardAgent({
175203 setUnreadWakes ( Math . max ( 0 , ( data . unreadWakes ?? 0 ) - unreadInView ) ) ;
176204
177205 const fresh = ( data . wakes ?? [ ] ) . filter ( ( wake ) => ! toastedWakes . current . has ( wake . watchId ) ) ;
178- for ( const wake of fresh ) toastedWakes . current . add ( wake . watchId ) ;
206+ for ( const wake of fresh ) rememberToasted ( wake . watchId ) ;
179207
180208 // A burst gets one summary toast: a stack of persistent toasts is a wall,
181209 // not a notification.
0 commit comments