From da943dcf02b844ed3511f4a58547ffc6d5840d29 Mon Sep 17 00:00:00 2001 From: Trevor Walker Date: Thu, 24 Sep 2026 05:33:42 -0600 Subject: [PATCH] fix(web): match notification icons to thread events --- .../ThreadNotificationCoordinator.test.tsx | 77 +++++++++++-------- .../ThreadNotificationCoordinator.tsx | 29 ++++++- 2 files changed, 74 insertions(+), 32 deletions(-) diff --git a/apps/web/src/components/ThreadNotificationCoordinator.test.tsx b/apps/web/src/components/ThreadNotificationCoordinator.test.tsx index 3c37fc3316..7e5dc2fc04 100644 --- a/apps/web/src/components/ThreadNotificationCoordinator.test.tsx +++ b/apps/web/src/components/ThreadNotificationCoordinator.test.tsx @@ -1,7 +1,13 @@ import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings"; import type { ClientSettings } from "@t3tools/contracts/settings"; import * as Option from "effect/Option"; -import { act } from "react"; +import { + CircleAlertIcon, + CircleCheckIcon, + MessageCircleQuestionIcon, + ShieldQuestionIcon, +} from "lucide-react"; +import { act, type ReactElement } from "react"; import { create, type ReactTestRenderer } from "react-test-renderer"; import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; @@ -23,8 +29,12 @@ const state = vi.hoisted(() => ({ sessionOnly: false, notifyCompletion: true, add: vi.fn( - (_toast: { title: string; description: string; actionProps: { onClick: () => void } }) => - "toast-1", + (_toast: { + title: string; + description: string; + data?: { leadingIcon?: ReactElement }; + actionProps: { onClick: () => void }; + }) => "toast-1", ), close: vi.fn(), navigate: vi.fn(), @@ -164,6 +174,7 @@ describe("thread notifications", () => { const toast = state.add.mock.calls[0]?.[0]; expect(toast?.title).toBe("Thread completed"); expect(toast?.description).toBe("Fix the login form"); + expect(toast?.data?.leadingIcon?.type).toBe(CircleCheckIcon); toast?.actionProps.onClick(); expect(state.close).toHaveBeenCalledWith("toast-1"); expect(state.navigate).toHaveBeenCalledWith({ @@ -188,35 +199,39 @@ describe("thread notifications", () => { ); it.each([ - ["input", "Input needed"], - ["approval", "Approval needed"], - ["sessionError", "Thread failed"], - ["turnError", "Thread failed"], - ] as const)("uses the same %s event for in-app and desktop alerts", async (event, title) => { - state.mode = "notifications-and-sound"; - await render(); - state[event] = true; - await render(); - await render(); - expect(state.add).toHaveBeenCalledTimes(1); - expect(state.add).toHaveBeenLastCalledWith(expect.objectContaining({ title })); - expect(state.sound).toHaveBeenCalledWith("input", expect.any(Function)); - expect(state.notification).not.toHaveBeenCalled(); + ["input", "Input needed", MessageCircleQuestionIcon], + ["approval", "Approval needed", ShieldQuestionIcon], + ["sessionError", "Thread failed", CircleAlertIcon], + ["turnError", "Thread failed", CircleAlertIcon], + ] as const)( + "uses the same %s event for in-app and desktop alerts", + async (event, title, icon) => { + state.mode = "notifications-and-sound"; + await render(); + state[event] = true; + await render(); + await render(); + expect(state.add).toHaveBeenCalledTimes(1); + expect(state.add).toHaveBeenLastCalledWith(expect.objectContaining({ title })); + expect(state.add.mock.calls[0]?.[0].data?.leadingIcon?.type).toBe(icon); + expect(state.sound).toHaveBeenCalledWith("input", expect.any(Function)); + expect(state.notification).not.toHaveBeenCalled(); - state[event] = false; - await render(); - state.focused = false; - state[event] = true; - await render(); - await render(); - expect(state.add).toHaveBeenCalledTimes(1); - expect(state.notification).toHaveBeenCalledTimes(1); - expect(state.notification).toHaveBeenCalledWith(title, { - body: "Fix the login form", - tag: "env-1:thread-1", - silent: true, - }); - }); + state[event] = false; + await render(); + state.focused = false; + state[event] = true; + await render(); + await render(); + expect(state.add).toHaveBeenCalledTimes(1); + expect(state.notification).toHaveBeenCalledTimes(1); + expect(state.notification).toHaveBeenCalledWith(title, { + body: "Fix the login form", + tag: "env-1:thread-1", + silent: true, + }); + }, + ); it("keeps background desktop alerts when in-app notifications are disabled", async () => { state.focused = false; diff --git a/apps/web/src/components/ThreadNotificationCoordinator.tsx b/apps/web/src/components/ThreadNotificationCoordinator.tsx index 6c3299dc8b..743d8f40f3 100644 --- a/apps/web/src/components/ThreadNotificationCoordinator.tsx +++ b/apps/web/src/components/ThreadNotificationCoordinator.tsx @@ -2,6 +2,12 @@ import { useAtomValue } from "@effect/atom-react"; import { useNavigate, useParams } from "@tanstack/react-router"; import type { EnvironmentId } from "@t3tools/contracts"; import * as Option from "effect/Option"; +import { + CircleAlertIcon, + CircleCheckIcon, + MessageCircleQuestionIcon, + ShieldQuestionIcon, +} from "lucide-react"; import { useCallback, useEffect, useRef } from "react"; import { getClientSettings, useClientSettings } from "../hooks/useSettings"; @@ -167,7 +173,28 @@ function EnvironmentNotifications({ type: kind === "completion" ? "success" : state.phase === "failed" ? "error" : "warning", title, description: state.threadTitle, - data: { hideCopyButton: true }, + data: { + hideCopyButton: true, + leadingIcon: + state.phase === "completed" ? ( + + ) : state.phase === "waiting_for_approval" ? ( + + ) : state.phase === "failed" ? ( + + ) : ( + + ), + }, actionProps: { children: "Open thread", onClick: () => {