Skip to content

Commit b9033f3

Browse files
committed
fix(webapp): the wake-toast dedupe survives reloads
The toast source is recent deliveries, so an in-memory dedupe re-toasted every wake younger than the window on every refresh.
1 parent 70559be commit b9033f3

1 file changed

Lines changed: 29 additions & 1 deletion

File tree

apps/webapp/app/components/dashboard-agent/DashboardAgent.tsx

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ import {
3030
// noticing within a minute, and the count is one indexed query.
3131
const 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

Comments
 (0)