Skip to content

Commit 4aa9abc

Browse files
committed
fix(webapp): wake toasts fire on delivery, not unread state; agent toast surface matches Ask Trigger on dark themes
A wake read on screen before the next poll never toasted. The toast list is now recent deliveries (15 min, id-deduped client-side); the dot still counts unread only.
1 parent 0c39e7a commit 4aa9abc

5 files changed

Lines changed: 26 additions & 11 deletions

File tree

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,12 @@ export function DashboardAgent({
167167
if (!res.ok) return;
168168
const data = (await res.json()) as { unreadWakes?: number; wakes?: WatchWake[] };
169169
if (cancelled) return;
170-
const wakesInView = (data.wakes ?? []).filter(
171-
(wake) => wake.chatId === visibleChat.current
170+
// The wakes list now carries READ ones too (they still toast, once) —
171+
// only unread ones in the visible chat are subtracted from the dot.
172+
const unreadInView = (data.wakes ?? []).filter(
173+
(wake) => wake.unread && wake.chatId === visibleChat.current
172174
).length;
173-
setUnreadWakes(Math.max(0, (data.unreadWakes ?? 0) - wakesInView));
175+
setUnreadWakes(Math.max(0, (data.unreadWakes ?? 0) - unreadInView));
174176

175177
const fresh = (data.wakes ?? []).filter((wake) => !toastedWakes.current.has(wake.watchId));
176178
for (const wake of fresh) toastedWakes.current.add(wake.watchId);

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ export type WatchWake = {
4242
identity?: string;
4343
resolution?: WatchResolution | null;
4444
observedOutcome?: WatchObservedOutcome | null;
45+
/** Landed after the chat's read marker. The dot counts these; the toast fires either way. */
46+
unread?: boolean;
4547
};
4648

4749
/**

apps/webapp/app/components/primitives/Toast.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,9 @@ export function ToastUI({
103103
"self-end rounded-md border border-grid-bright bg-background-dimmed",
104104
variant === "success" && "border-success",
105105
variant === "error" && "border-error",
106-
// The agent's toast wears the Ask Trigger button's border.
107-
variant === "agent" && "border-[#41FF54]/25 light:border-success/60"
106+
// The agent's toast wears the Ask Trigger button's border, and on the
107+
// dark themes its surface too.
108+
variant === "agent" && "border-[#41FF54]/25 light:border-success/60 dark:bg-secondary"
108109
)}
109110
style={{
110111
width: toastWidth,

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.dashboard-agent.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
listChatIdsWithOpenInvestigations,
1313
listChatIdsWithUnreadWakes,
1414
listChats,
15-
listUnreadWatchWakes,
15+
listRecentWatchWakes,
1616
markChatRead,
1717
renameChat,
1818
setChatPinned,
@@ -120,15 +120,18 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
120120
const searchParams = new URL(request.url).searchParams;
121121

122122
if (searchParams.get("unread") === "1") {
123-
// The count drives the dot, the capped list drives one toast per wake.
123+
// The count drives the dot; the list drives one toast per wake. The list is
124+
// RECENT deliveries rather than unread ones — a wake the user happened to be
125+
// reading when it landed still deserves its toast (client dedupes by id).
124126
const [unreadWakes, wakes] = await Promise.all([
125127
countUnreadWatchWakes(dashboardAgentDb, {
126128
organizationId: project.organizationId,
127129
userId,
128130
}),
129-
listUnreadWatchWakes(dashboardAgentDb, {
131+
listRecentWatchWakes(dashboardAgentDb, {
130132
organizationId: project.organizationId,
131133
userId,
134+
deliveredAfter: new Date(Date.now() - 15 * 60 * 1000),
132135
}),
133136
]);
134137
return json({ unreadWakes, wakes });

internal-packages/dashboard-agent-db/src/queries.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,8 @@ export interface UnreadWatchWake {
10501050
/** Null on a row written before the resolution model — the surface falls back. */
10511051
resolution: WatchResolution | null;
10521052
observedOutcome: WatchObservedOutcome | null;
1053+
/** Landed after the chat's read marker. The dot counts these; the toast fires either way. */
1054+
unread: boolean;
10531055
}
10541056

10551057
// The toast fires one per wake, so a long-unopened panel doesn't need the whole
@@ -1062,11 +1064,14 @@ const UNREAD_WAKE_LIST_LIMIT = 10;
10621064
* this one returns rows instead of a total, capped at
10631065
* {@link UNREAD_WAKE_LIST_LIMIT}.
10641066
*/
1065-
export async function listUnreadWatchWakes(
1067+
export async function listRecentWatchWakes(
10661068
db: DashboardAgentDb,
1067-
params: { organizationId: string; userId: string }
1069+
params: { organizationId: string; userId: string; deliveredAfter: Date }
10681070
): Promise<UnreadWatchWake[]> {
10691071
const resolvedAt = sql<Date>`coalesce(${watches.firedAt}, ${watches.lastCheckedAt})`;
1072+
// Whether the wake landed after the chat was last read. The TOAST doesn't
1073+
// care (a wake read on screen still deserves its toast, once); the DOT does.
1074+
const unread = sql<boolean>`(${chats.lastReadAt} is null or coalesce(${watches.firedAt}, ${watches.lastCheckedAt}) > ${chats.lastReadAt})`;
10701075

10711076
const rows = await db
10721077
.select({
@@ -1078,6 +1083,7 @@ export async function listUnreadWatchWakes(
10781083
resolution: watches.resolution,
10791084
observedOutcome: watches.observedOutcome,
10801085
resolvedAt,
1086+
unread,
10811087
})
10821088
.from(watches)
10831089
.innerJoin(chats, eq(chats.id, watches.chatId))
@@ -1091,7 +1097,7 @@ export async function listUnreadWatchWakes(
10911097
eq(chats.organizationId, params.organizationId),
10921098
eq(chats.userId, params.userId),
10931099
isNull(chats.deletedAt),
1094-
sql`(${chats.lastReadAt} is null or coalesce(${watches.firedAt}, ${watches.lastCheckedAt}) > ${chats.lastReadAt})`
1100+
sql`coalesce(${watches.firedAt}, ${watches.lastCheckedAt}) > ${params.deliveredAfter.toISOString()}::timestamptz`
10951101
)
10961102
)
10971103
.orderBy(desc(resolvedAt))
@@ -1108,6 +1114,7 @@ export async function listUnreadWatchWakes(
11081114
identity: row.identity,
11091115
resolution: row.resolution,
11101116
observedOutcome: row.observedOutcome,
1117+
unread: row.unread,
11111118
}));
11121119
}
11131120

0 commit comments

Comments
 (0)