import type { Bot, Group, RunActivityRow } from "@rakazo/contracts"; import { isActionableRun, isDoingStatus, isFailedRoutine } from "./agent-activity"; export type JetztRunItem = { kind: "run"; run: RunActivityRow; }; export type JetztUnreadItem = { kind: "unread"; chatKind: "bot" | "group"; chat: Bot | Group; }; export type JetztItem = JetztRunItem | JetztUnreadItem; export type DoingItem = { kind: "run"; run: RunActivityRow; }; export type DoneItem = { kind: "run"; run: RunActivityRow; }; /** @deprecated Use needsYou — kept for gradual migration in tests. */ export type WeiterItem = { chatKind: "bot" | "group"; chat: Bot | Group; }; export type HeuteBriefData = { /** Needs you — capped at three visible rows. */ needsYou: JetztItem[]; /** Active runs where the agent is working, not blocked. */ doing: DoingItem[]; /** Recently completed runs from today. */ done: DoneItem[]; /** Total actionable count (may exceed visible cap). */ actionCount: number; /** @deprecated Alias for needsYou — legacy bucket name. */ jetzt: JetztItem[]; /** @deprecated Empty — replaced by doing/done buckets. */ weiter: WeiterItem[]; }; export const HEUTE_NEEDS_YOU_CAP = 3; export function isToday(iso: string, now = new Date()): boolean { const date = new Date(iso); if (Number.isNaN(date.getTime())) return false; return ( date.getDate() === now.getDate() && date.getMonth() === now.getMonth() && date.getFullYear() === now.getFullYear() ); } export function cleanBriefPreview(raw?: string | null): string { if (!raw) return ""; let text = raw; text = text.replace(/^#{1,6}\s+/gm, ""); text = text.replace(/\[(?:to|RECOVERY PROTOCOL|PO)[^\]]*\]/gi, ""); text = text.replace(/[*_`#]/g, ""); text = text.replace(/\\/g, ""); return text.replace(/\s+/g, " ").trim(); } function runTargetKey(run: RunActivityRow): string { return run.groupId ? `group:${run.groupId}` : `bot:${run.botId}`; } function chatTargetKey(chatKind: "bot" | "group", chatId: string): string { return chatKind === "group" ? `group:${chatId}` : `bot:${chatId}`; } export function buildHeuteBrief(params: { bots: Bot[]; groups: Group[]; activeRuns: RunActivityRow[]; recentRuns: RunActivityRow[]; now?: Date; }): HeuteBriefData { const { bots, groups, activeRuns, recentRuns, now = new Date() } = params; const needsYouAll: JetztItem[] = []; const coveredTargets = new Set(); const actionableRuns = [ ...activeRuns.filter(isActionableRun), ...recentRuns.filter(isFailedRoutine), ]; const seenRunIds = new Set(); for (const run of actionableRuns) { if (seenRunIds.has(run.runId)) continue; seenRunIds.add(run.runId); needsYouAll.push({ kind: "run", run }); coveredTargets.add(runTargetKey(run)); } const unreadChats: JetztUnreadItem[] = [ ...bots .filter((bot) => bot.unread) .map((bot) => ({ kind: "unread" as const, chatKind: "bot" as const, chat: bot })), ...groups .filter((group) => group.unread) .map((group) => ({ kind: "unread" as const, chatKind: "group" as const, chat: group, })), ]; for (const item of unreadChats) { const key = chatTargetKey(item.chatKind, item.chat.id); if (coveredTargets.has(key)) continue; needsYouAll.push(item); coveredTargets.add(key); } const doing: DoingItem[] = activeRuns .filter((run) => isDoingStatus(run.status)) .map((run) => ({ kind: "run" as const, run })); const done: DoneItem[] = recentRuns .filter((run) => run.status === "completed" && isToday(run.updatedAt, now)) .slice(0, 5) .map((run) => ({ kind: "run" as const, run })); const needsYou = needsYouAll.slice(0, HEUTE_NEEDS_YOU_CAP); return { needsYou, doing, done, actionCount: needsYouAll.length, jetzt: needsYouAll, weiter: [], }; }