agent-ecosystem/src/main/utils/teamNotificationBuilder.ts

114 lines
4.7 KiB
TypeScript

/**
* Team notification builder — creates DetectedError objects from team event payloads.
*
* Pure utility with no service dependencies. Used by NotificationManager.addTeamNotification()
* to convert domain-level team payloads into the unified notification format.
*/
import { stripAgentBlocks } from '@shared/constants/agentBlocks';
import { randomUUID } from 'crypto';
import type { DetectedError } from '../services/error/ErrorMessageBuilder';
import type { TriggerColor } from '@shared/constants/triggerColors';
import type { NotificationTarget, TeamEventType } from '@shared/types/notifications';
// Re-export for callers that import TeamEventType from this module
export type { TeamEventType } from '@shared/types/notifications';
// =============================================================================
// Types
// =============================================================================
/**
* Domain payload for team notifications.
* Single source of truth — both storage and native presentation are derived from this.
*/
export interface TeamNotificationPayload {
teamEventType: TeamEventType;
teamName: string;
teamDisplayName: string;
from: string;
to?: string;
/** Optional team roster for resolving the same participant avatar shown in the UI. */
members?: readonly {
name: string;
removedAt?: number | string | null;
agentType?: string;
}[];
summary: string;
body: string;
/** Stable key for storage deduplication. REQUIRED — no fallback to Date.now(). */
dedupeKey: string;
/** Structured destination used by notification click handling. */
target?: NotificationTarget;
projectPath?: string;
/**
* When true, the notification is stored in-app but no native OS toast is shown.
* Used when per-type toggle (e.g. notifyOnLeadInbox) is off — storage is unconditional,
* but the user opted out of OS interruptions for this event type.
*/
suppressToast?: boolean;
}
// =============================================================================
// Config mapping
// =============================================================================
interface TeamNotificationConfig {
triggerName: string;
triggerColor: TriggerColor;
}
const TEAM_NOTIFICATION_CONFIG: Record<TeamEventType, TeamNotificationConfig> = {
rate_limit: { triggerName: 'Rate Limit', triggerColor: 'red' },
api_error: { triggerName: 'API Error', triggerColor: 'red' },
lead_inbox: { triggerName: 'Team Inbox', triggerColor: 'blue' },
user_inbox: { triggerName: 'User Inbox', triggerColor: 'green' },
task_clarification: { triggerName: 'Clarification', triggerColor: 'orange' },
task_status_change: { triggerName: 'Status Change', triggerColor: 'purple' },
task_comment: { triggerName: 'Task Comment', triggerColor: 'cyan' },
task_review_requested: { triggerName: 'Review Requested', triggerColor: 'orange' },
task_blocked: { triggerName: 'Task Blocked', triggerColor: 'red' },
task_created: { triggerName: 'Task Created', triggerColor: 'green' },
all_tasks_completed: { triggerName: 'All Done', triggerColor: 'green' },
cross_team_message: { triggerName: 'Cross-Team', triggerColor: 'cyan' },
schedule_completed: { triggerName: 'Schedule Done', triggerColor: 'green' },
schedule_failed: { triggerName: 'Schedule Failed', triggerColor: 'red' },
team_launched: { triggerName: 'Team Launched', triggerColor: 'green' },
team_launch_incomplete: { triggerName: 'Launch Incomplete', triggerColor: 'orange' },
};
// =============================================================================
// Builder
// =============================================================================
/**
* Converts a team notification payload into a DetectedError for unified storage.
* Uses `sessionId: 'team:{teamName}'` convention (established by rate-limit notifications).
*/
export function buildDetectedErrorFromTeam(payload: TeamNotificationPayload): DetectedError {
const config = TEAM_NOTIFICATION_CONFIG[payload.teamEventType];
const summary = stripAgentBlocks(payload.summary).replace(/\s+/g, ' ').trim();
const body = stripAgentBlocks(payload.body).replace(/\s+/g, ' ').trim();
const preview = summary && body ? `${summary}: ${body}` : summary || body;
return {
id: randomUUID(),
timestamp: Date.now(),
sessionId: `team:${payload.teamName}`,
projectId: payload.teamName,
filePath: '',
source: payload.teamEventType,
message: `[${payload.from}] ${preview.slice(0, 300)}`,
category: 'team',
teamEventType: payload.teamEventType,
target: payload.target,
dedupeKey: payload.dedupeKey,
triggerColor: config.triggerColor,
triggerName: config.triggerName,
context: {
projectName: payload.teamDisplayName,
cwd: payload.projectPath,
},
};
}