import type { ConversationSummary } from "$lib/api/types"; import { parseDate } from "$lib/format"; import { byActivity } from "$lib/shell/state"; export interface DayGroup { branches: ConversationSummary[]; day: string; deep: ConversationSummary[]; forks: ConversationSummary[]; jobs: ConversationSummary[]; label: string; live: boolean; master: ConversationSummary | null; others: ConversationSummary[]; } const DAY_MS = 86_400_000; function localDay(iso: string | null | undefined): string { const date = parseDate(iso) ?? new Date(); const y = date.getFullYear(); const m = String(date.getMonth() + 1).padStart(2, "0"); const d = String(date.getDate()).padStart(2, "0"); return `${y}-${m}-${d}`; } export function dayLabel(day: string, now = new Date()): string { const today = localDay(now.toISOString()); const yesterday = localDay(new Date(now.getTime() - DAY_MS).toISOString()); if (day === today) { return "Today"; } if (day === yesterday) { return "Yesterday"; } const date = new Date(`${day}T12:00:00`); return date.toLocaleDateString(undefined, { day: "numeric", month: "short", weekday: "short", }); } function activityOf(row: ConversationSummary): string { return row.last_activity_at ?? row.created_at ?? ""; } // Days as the operator remembers them: the master that began that day, // and under it everything that last moved that day - branches, deep // chats, forks, job runs. Open branches follow the master through the // rotation, so their parent says nothing about when they were alive; // the day of their last word does. export function groupByDay( rows: ConversationSummary[], now = new Date() ): DayGroup[] { const groups = new Map(); const group = (day: string): DayGroup => { let found = groups.get(day); if (!found) { found = { branches: [], day, deep: [], forks: [], jobs: [], label: dayLabel(day, now), live: false, master: null, others: [], }; groups.set(day, found); } return found; }; const masters = rows .filter((row) => row.kind === "master") .sort((a, b) => (b.created_at ?? "").localeCompare(a.created_at ?? "")); for (const master of masters) { const g = group(localDay(master.created_at)); if (!g.master || master.status === "open") { g.master = master; } else { g.others.push(master); } } for (const row of rows) { if (row.kind === "master") { continue; } const g = group(localDay(activityOf(row))); if (row.kind === "branch") { g.branches.push(row); } else if (row.kind === "deep") { g.deep.push(row); } else if (row.kind === "fork") { g.forks.push(row); } else if (row.kind === "job") { g.jobs.push(row); } else { g.others.push(row); } } const out = [...groups.values()]; for (const g of out) { g.branches.sort(byActivity); g.deep.sort(byActivity); g.forks.sort(byActivity); g.jobs.sort(byActivity); g.others.sort(byActivity); g.live = [ g.master, ...g.branches, ...g.deep, ...g.forks, ...g.jobs, ...g.others, ].some((row) => row && (row.running_turn || row.pending_question)); } return out.sort((a, b) => b.day.localeCompare(a.day)); } // One phrase for the day header: what the day held, and whether its // master is still the live one. export function daySummary(group: DayGroup, today: boolean): string { const parts: string[] = []; if (group.master?.status === "open" && !today) { parts.push("master still open"); } const count = (n: number, one: string, many = `${one}s`) => `${n} ${n === 1 ? one : many}`; if (group.branches.length) { parts.push(count(group.branches.length, "branch", "branches")); } if (group.deep.length) { parts.push(count(group.deep.length, "deep", "deep")); } if (group.forks.length) { parts.push(count(group.forks.length, "fork")); } if (group.jobs.length) { parts.push(count(group.jobs.length, "job run")); } if (group.others.length) { parts.push(count(group.others.length, "more", "more")); } return parts.join(" ยท "); }