From 48033ef7016b9b5a41c64ca146794356f446ae39 Mon Sep 17 00:00:00 2001 From: iliya Date: Mon, 16 Mar 2026 14:01:16 +0200 Subject: [PATCH] feat: add task created notification type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New notification fires when agents create tasks, with toggle in settings. Follows existing pattern: detect → fire → seed on initial load. --- src/main/ipc/configValidation.ts | 7 ++++ .../services/error/ErrorMessageBuilder.ts | 1 + .../services/infrastructure/ConfigManager.ts | 3 ++ src/main/utils/teamNotificationBuilder.ts | 2 + .../settings/hooks/useSettingsConfig.ts | 2 + .../settings/hooks/useSettingsHandlers.ts | 1 + .../sections/NotificationsSection.tsx | 13 +++++++ src/renderer/store/slices/teamSlice.ts | 38 +++++++++++++++++++ src/shared/types/notifications.ts | 3 ++ src/shared/types/team.ts | 2 +- .../utils/teamNotificationBuilder.test.ts | 1 + 11 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/main/ipc/configValidation.ts b/src/main/ipc/configValidation.ts index 5d08014d..f2a122d7 100644 --- a/src/main/ipc/configValidation.ts +++ b/src/main/ipc/configValidation.ts @@ -114,6 +114,7 @@ function validateNotificationsSection( 'snoozeMinutes', 'notifyOnStatusChange', 'notifyOnTaskComments', + 'notifyOnTaskCreated', 'statusChangeOnlySolo', 'statusChangeStatuses', 'triggers', @@ -178,6 +179,12 @@ function validateNotificationsSection( } result.notifyOnTaskComments = value; break; + case 'notifyOnTaskCreated': + if (typeof value !== 'boolean') { + return { valid: false, error: `notifications.${key} must be a boolean` }; + } + result.notifyOnTaskCreated = value; + break; case 'statusChangeOnlySolo': if (typeof value !== 'boolean') { return { valid: false, error: `notifications.${key} must be a boolean` }; diff --git a/src/main/services/error/ErrorMessageBuilder.ts b/src/main/services/error/ErrorMessageBuilder.ts index 88b0e3ed..67994b31 100644 --- a/src/main/services/error/ErrorMessageBuilder.ts +++ b/src/main/services/error/ErrorMessageBuilder.ts @@ -59,6 +59,7 @@ export interface DetectedError { | 'task_clarification' | 'task_status_change' | 'task_comment' + | 'task_created' | 'schedule_completed' | 'schedule_failed'; /** Explicit key for storage deduplication. Two notifications with the same dedupeKey won't be stored twice. */ diff --git a/src/main/services/infrastructure/ConfigManager.ts b/src/main/services/infrastructure/ConfigManager.ts index aa656456..bce97aa8 100644 --- a/src/main/services/infrastructure/ConfigManager.ts +++ b/src/main/services/infrastructure/ConfigManager.ts @@ -50,6 +50,8 @@ export interface NotificationConfig { notifyOnStatusChange: boolean; /** Whether to show native OS notifications when a new comment is added to a task */ notifyOnTaskComments: boolean; + /** Whether to show native OS notifications when a new task is created */ + notifyOnTaskCreated: boolean; /** Only notify on status changes in solo teams (no teammates) */ statusChangeOnlySolo: boolean; /** Which target statuses to notify about (e.g. ['in_progress', 'completed']) */ @@ -264,6 +266,7 @@ const DEFAULT_CONFIG: AppConfig = { notifyOnClarifications: true, notifyOnStatusChange: true, notifyOnTaskComments: true, + notifyOnTaskCreated: true, statusChangeOnlySolo: false, statusChangeStatuses: ['in_progress', 'completed'], triggers: DEFAULT_TRIGGERS, diff --git a/src/main/utils/teamNotificationBuilder.ts b/src/main/utils/teamNotificationBuilder.ts index 6f49c8d7..eab6ffdf 100644 --- a/src/main/utils/teamNotificationBuilder.ts +++ b/src/main/utils/teamNotificationBuilder.ts @@ -21,6 +21,7 @@ export type TeamEventType = | 'task_clarification' | 'task_status_change' | 'task_comment' + | 'task_created' | 'schedule_completed' | 'schedule_failed'; @@ -63,6 +64,7 @@ const TEAM_NOTIFICATION_CONFIG: Record = task_clarification: { triggerName: 'Clarification', triggerColor: 'orange' }, task_status_change: { triggerName: 'Status Change', triggerColor: 'purple' }, task_comment: { triggerName: 'Task Comment', triggerColor: 'cyan' }, + task_created: { triggerName: 'Task Created', triggerColor: 'green' }, schedule_completed: { triggerName: 'Schedule Done', triggerColor: 'green' }, schedule_failed: { triggerName: 'Schedule Failed', triggerColor: 'red' }, }; diff --git a/src/renderer/components/settings/hooks/useSettingsConfig.ts b/src/renderer/components/settings/hooks/useSettingsConfig.ts index 6917c455..78473019 100644 --- a/src/renderer/components/settings/hooks/useSettingsConfig.ts +++ b/src/renderer/components/settings/hooks/useSettingsConfig.ts @@ -47,6 +47,7 @@ export interface SafeConfig { notifyOnClarifications: boolean; notifyOnStatusChange: boolean; notifyOnTaskComments: boolean; + notifyOnTaskCreated: boolean; statusChangeOnlySolo: boolean; statusChangeStatuses: string[]; triggers: AppConfig['notifications']['triggers']; @@ -181,6 +182,7 @@ export function useSettingsConfig(): UseSettingsConfigReturn { notifyOnClarifications: displayConfig?.notifications?.notifyOnClarifications ?? true, notifyOnStatusChange: displayConfig?.notifications?.notifyOnStatusChange ?? true, notifyOnTaskComments: displayConfig?.notifications?.notifyOnTaskComments ?? true, + notifyOnTaskCreated: displayConfig?.notifications?.notifyOnTaskCreated ?? true, statusChangeOnlySolo: displayConfig?.notifications?.statusChangeOnlySolo ?? true, statusChangeStatuses: displayConfig?.notifications?.statusChangeStatuses ?? [ 'in_progress', diff --git a/src/renderer/components/settings/hooks/useSettingsHandlers.ts b/src/renderer/components/settings/hooks/useSettingsHandlers.ts index 5e469136..431b451e 100644 --- a/src/renderer/components/settings/hooks/useSettingsHandlers.ts +++ b/src/renderer/components/settings/hooks/useSettingsHandlers.ts @@ -300,6 +300,7 @@ export function useSettingsHandlers({ notifyOnClarifications: true, notifyOnStatusChange: true, notifyOnTaskComments: true, + notifyOnTaskCreated: true, statusChangeOnlySolo: true, statusChangeStatuses: ['in_progress', 'completed'], triggers: defaultTriggers, diff --git a/src/renderer/components/settings/sections/NotificationsSection.tsx b/src/renderer/components/settings/sections/NotificationsSection.tsx index 0ca31d22..52b7e565 100644 --- a/src/renderer/components/settings/sections/NotificationsSection.tsx +++ b/src/renderer/components/settings/sections/NotificationsSection.tsx @@ -23,6 +23,7 @@ import { Mail, MessageSquare, PartyPopper, + CirclePlus, Send, Users, Volume2, @@ -64,6 +65,7 @@ interface NotificationsSectionProps { | 'notifyOnClarifications' | 'notifyOnStatusChange' | 'notifyOnTaskComments' + | 'notifyOnTaskCreated' | 'statusChangeOnlySolo', value: boolean ) => void; @@ -293,6 +295,17 @@ export const NotificationsSection = ({ disabled={saving || !safeConfig.notifications.enabled} /> + } + > + onNotificationToggle('notifyOnTaskCreated', v)} + disabled={saving || !safeConfig.notifications.enabled} + /> + {/* Task Status Change Notifications — nested within team card */}
diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts index a155803a..1658000c 100644 --- a/src/renderer/store/slices/teamSlice.ts +++ b/src/renderer/store/slices/teamSlice.ts @@ -116,6 +116,7 @@ import type { StateCreator } from 'zustand'; const notifiedClarificationTaskKeys = new Set(); const notifiedStatusChangeKeys = new Set(); const notifiedCommentKeys = new Set(); +const notifiedCreatedTaskKeys = new Set(); let isFirstFetchAllTasks = true; @@ -295,6 +296,39 @@ function fireTaskCommentNotification( .catch(() => undefined); } +function detectTaskCreatedNotifications( + oldTasks: GlobalTask[], + newTasks: GlobalTask[], + notifyEnabled: boolean +): void { + const oldTaskKeys = new Set(oldTasks.map((t) => `${t.teamName}:${t.id}`)); + + for (const task of newTasks) { + const key = `${task.teamName}:${task.id}`; + if (oldTaskKeys.has(key)) continue; + if (notifiedCreatedTaskKeys.has(key)) continue; + notifiedCreatedTaskKeys.add(key); + + fireTaskCreatedNotification(task, !notifyEnabled); + } +} + +function fireTaskCreatedNotification(task: GlobalTask, suppressToast: boolean): void { + void api.teams + ?.showMessageNotification({ + teamName: task.teamName, + teamDisplayName: task.teamDisplayName, + from: task.owner ?? 'system', + to: 'user', + summary: `New task ${formatTaskDisplayLabel(task)}: ${task.subject}`, + body: task.description || task.subject, + teamEventType: 'task_created', + dedupeKey: `created:${task.teamName}:${task.id}`, + suppressToast, + }) + .catch(() => undefined); +} + function collectTaskChangeInvalidationState( teamName: string, prevTasks: TeamData['tasks'], @@ -852,6 +886,8 @@ export const createTeamSlice: StateCreator = (set, detectStatusChangeNotifications(oldTasks, tasks, get().appConfig, get().teamByName); const notifyOnTaskComments = get().appConfig?.notifications?.notifyOnTaskComments ?? true; detectTaskCommentNotifications(oldTasks, tasks, notifyOnTaskComments); + const notifyOnTaskCreated = get().appConfig?.notifications?.notifyOnTaskCreated ?? true; + detectTaskCreatedNotifications(oldTasks, tasks, notifyOnTaskCreated); } else { // Initial load — seed the Sets to prevent false notifications on next update for (const task of tasks) { @@ -869,6 +905,8 @@ export const createTeamSlice: StateCreator = (set, for (const comment of task.comments ?? []) { notifiedCommentKeys.add(`${task.teamName}:${task.id}:${comment.id}`); } + // Seed created task keys to prevent false notifications + notifiedCreatedTaskKeys.add(`${task.teamName}:${task.id}`); } } diff --git a/src/shared/types/notifications.ts b/src/shared/types/notifications.ts index ab86408f..95c6eb43 100644 --- a/src/shared/types/notifications.ts +++ b/src/shared/types/notifications.ts @@ -60,6 +60,7 @@ export interface DetectedError { | 'task_clarification' | 'task_status_change' | 'task_comment' + | 'task_created' | 'schedule_completed' | 'schedule_failed'; /** Explicit key for storage deduplication. Two notifications with the same dedupeKey won't be stored twice. */ @@ -271,6 +272,8 @@ export interface AppConfig { notifyOnStatusChange: boolean; /** Whether to show native OS notifications when a new comment is added to a task */ notifyOnTaskComments: boolean; + /** Whether to show native OS notifications when a new task is created */ + notifyOnTaskCreated: boolean; /** Only notify on status changes in solo teams (no teammates) */ statusChangeOnlySolo: boolean; /** Which target statuses to notify about (e.g. ['in_progress', 'completed']) */ diff --git a/src/shared/types/team.ts b/src/shared/types/team.ts index dd6c4014..76d7fbd2 100644 --- a/src/shared/types/team.ts +++ b/src/shared/types/team.ts @@ -713,7 +713,7 @@ export interface TeamMessageNotificationData { /** Optional sender color for visual context. */ color?: string; /** Team event sub-type for notification categorization. */ - teamEventType?: 'task_clarification' | 'task_status_change' | 'task_comment'; + teamEventType?: 'task_clarification' | 'task_status_change' | 'task_comment' | 'task_created'; /** Stable key for storage deduplication. Required — no fallback to Date.now(). */ dedupeKey?: string; /** diff --git a/test/main/utils/teamNotificationBuilder.test.ts b/test/main/utils/teamNotificationBuilder.test.ts index 1636af2a..2b394840 100644 --- a/test/main/utils/teamNotificationBuilder.test.ts +++ b/test/main/utils/teamNotificationBuilder.test.ts @@ -93,6 +93,7 @@ describe('buildDetectedErrorFromTeam', () => { task_clarification: { triggerName: 'Clarification', triggerColor: 'orange' }, task_status_change: { triggerName: 'Status Change', triggerColor: 'purple' }, task_comment: { triggerName: 'Task Comment', triggerColor: 'cyan' }, + task_created: { triggerName: 'Task Created', triggerColor: 'green' }, schedule_completed: { triggerName: 'Schedule Done', triggerColor: 'green' }, schedule_failed: { triggerName: 'Schedule Failed', triggerColor: 'red' }, };