diff --git a/src/features/workbench/api/operational-task-client.ts b/src/features/workbench/api/operational-task-client.ts index 5235cda..72834b3 100644 --- a/src/features/workbench/api/operational-task-client.ts +++ b/src/features/workbench/api/operational-task-client.ts @@ -198,7 +198,18 @@ const operationalTaskSchema = taskSummarySchema.extend({ const notificationSchema = z.object({ notification_id: z.string().uuid(), task_id: z.string().uuid(), - kind: z.enum(["task_created", "action_pending", "report_updated", "task_completed", "task_failed", "run_delivery_degraded"]), + kind: z.enum([ + "task_created", + "action_pending", + "report_updated", + "task_completed", + "task_failed", + "run_delivery_degraded", + "access_approval_pending", + "action_approval_pending", + "human_input_required", + "external_coordination_failed" + ]), title: z.string().min(1), description: z.string().min(1), status: z.enum(["unread", "read"]), diff --git a/src/features/workbench/map-workbench-page.tsx b/src/features/workbench/map-workbench-page.tsx index 04fa4fb..bf9ff92 100644 --- a/src/features/workbench/map-workbench-page.tsx +++ b/src/features/workbench/map-workbench-page.tsx @@ -110,6 +110,7 @@ import type { WorkbenchUser } from "./types"; import { getScheduledConditionIdFromAlertId } from "./utils/scheduled-condition-alerts"; +import { getOperationalNotificationRoute } from "./utils/operational-notification-routing"; import { createAlertQueueConversationPrompt } from "./utils/scheduled-condition-prompts"; const WORKBENCH_LAYOUT_CSS_VARIABLES = getWorkbenchLayoutCssVariables(); @@ -260,15 +261,18 @@ export function MapWorkbenchPage({ return condition; }, []); const workbenchAlerts = useMemo(() => { - const taskNotifications = operationalTasks.notifications.map((notification) => ({ - id: `operational-task-notification:${notification.notification_id}`, - conditionId: operationalTaskConditionId(notification.task_id), - title: notification.title, - description: notification.description, - severity: notification.kind === "task_failed" ? "critical" as const : "warning" as const, - occurredAt: notification.created_at, - time: new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false }).format(new Date(notification.created_at)) - })); + const taskNotifications = operationalTasks.notifications.flatMap((notification) => { + const route = getOperationalNotificationRoute(notification.kind); + return route ? [{ + id: `operational-task-notification:${notification.notification_id}`, + conditionId: operationalTaskConditionId(notification.task_id), + title: notification.title, + description: notification.description, + severity: route.severity, + occurredAt: notification.created_at, + time: new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false }).format(new Date(notification.created_at)) + }] : []; + }); return operationalTasks.available ? [...taskNotifications, ...runtimeWorkbenchAlerts] : runtimeWorkbenchAlerts; }, [operationalTasks.available, operationalTasks.notifications, runtimeWorkbenchAlerts]); const collapseAgentPanel = agent.collapsePanel; diff --git a/src/features/workbench/utils/operational-notification-routing.test.ts b/src/features/workbench/utils/operational-notification-routing.test.ts new file mode 100644 index 0000000..8d9c7d0 --- /dev/null +++ b/src/features/workbench/utils/operational-notification-routing.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { getOperationalNotificationRoute } from "./operational-notification-routing"; + +describe("getOperationalNotificationRoute", () => { + it.each(["task_created", "report_updated", "task_completed"] as const)( + "keeps normal lifecycle notification %s out of exception handling", + (kind) => expect(getOperationalNotificationRoute(kind)).toBeNull() + ); + + it("routes failures, approvals, human input, and external coordination", () => { + expect(getOperationalNotificationRoute("task_failed")).toEqual({ + category: "task_exception", + severity: "critical" + }); + expect(getOperationalNotificationRoute("access_approval_pending")?.category) + .toBe("human_approval"); + expect(getOperationalNotificationRoute("action_approval_pending")?.category) + .toBe("human_approval"); + expect(getOperationalNotificationRoute("human_input_required")?.category) + .toBe("human_input"); + expect(getOperationalNotificationRoute("external_coordination_failed")).toEqual({ + category: "external_coordination", + severity: "critical" + }); + }); + + it("keeps legacy action notifications visible during the database rollout", () => { + expect(getOperationalNotificationRoute("action_pending")?.category) + .toBe("human_approval"); + }); +}); diff --git a/src/features/workbench/utils/operational-notification-routing.ts b/src/features/workbench/utils/operational-notification-routing.ts new file mode 100644 index 0000000..d413d69 --- /dev/null +++ b/src/features/workbench/utils/operational-notification-routing.ts @@ -0,0 +1,33 @@ +import type { OperationalTaskNotification } from "../api/operational-task-client"; + +export type OperationalNotificationCategory = + | "task_exception" + | "human_approval" + | "human_input" + | "external_coordination"; + +export type OperationalNotificationRoute = { + category: OperationalNotificationCategory; + severity: "warning" | "critical"; +}; + +const NOTIFICATION_ROUTES: Partial< + Record +> = { + task_failed: { category: "task_exception", severity: "critical" }, + run_delivery_degraded: { category: "task_exception", severity: "critical" }, + action_pending: { category: "human_approval", severity: "warning" }, + access_approval_pending: { category: "human_approval", severity: "warning" }, + action_approval_pending: { category: "human_approval", severity: "warning" }, + human_input_required: { category: "human_input", severity: "warning" }, + external_coordination_failed: { + category: "external_coordination", + severity: "critical" + } +}; + +export function getOperationalNotificationRoute( + kind: OperationalTaskNotification["kind"] +): OperationalNotificationRoute | null { + return NOTIFICATION_ROUTES[kind] ?? null; +}