// Pure helpers behind the spawn form's project picker (#59): staleness, // recency ordering, worktree grouping, and free-text filtering. The daemon // reports the timestamps; what "stale" means is a client decision (#54). import type { Project } from "./types.js"; /** Projects whose last activity is older than this are dimmed and sort after * fresh ones — still reachable, visibly stale (#59). */ export const STALE_AFTER_DAYS = 7; export function isStale(project: Project, now: Date): boolean { if (!project.last_activity) return true; const age = now.getTime() - Date.parse(project.last_activity); return !(age >= 0) || age > STALE_AFTER_DAYS * 24 * 60 * 60 * 1000; } /** Fresh rows first, each block newest-activity-first; unknown or future * timestamps sort as stalest. Ties fall back to name so the order is stable. */ export function sortProjects(projects: Project[], now: Date): Project[] { return [...projects].sort((a, b) => { const staleDiff = (isStale(a, now) ? 1 : 0) - (isStale(b, now) ? 1 : 0); if (staleDiff !== 0) return staleDiff; const ta = timeOf(a); const tb = timeOf(b); if (ta !== tb) return tb - ta; return a.name.localeCompare(b.name) || a.path.localeCompare(b.path); }); } function timeOf(project: Project): number { return project.last_activity ? Date.parse(project.last_activity) : Number.NEGATIVE_INFINITY; } /** One repository group: the main checkout followed by its linked worktrees. * A group's position follows its freshest member, since a worktree worked on * today should pull its (dormant) base repo up with it. */ export interface ProjectGroup { main: Project; worktrees: Project[]; } /** Group `sorted` projects by their base repository. A worktree nests under * its main checkout even when the main sorts later; a main checkout missing * from the list doesn't drop its worktrees. */ export function groupProjects(sorted: Project[]): ProjectGroup[] { const members = new Map(); const keys: string[] = []; for (const p of sorted) { const key = p.base_repo ?? p.path; let list = members.get(key); if (!list) { list = []; members.set(key, list); keys.push(key); } list.push(p); } return keys.map((key) => { const list = members.get(key)!; const main = list.find((p) => p.path === key && !p.is_worktree) ?? list[0]; // Keep the daemon's newest-first order within the group. const worktrees = list .filter((p) => p !== main) .sort((a, b) => timeOf(b) - timeOf(a)); return { main, worktrees }; }); } /** Substring filter over project name and full path, case-insensitive. */ export function filterProjects(projects: Project[], query: string): Project[] { const q = query.trim().toLowerCase(); if (!q) return [...projects]; return projects.filter( (p) => p.name.toLowerCase().includes(q) || p.path.toLowerCase().includes(q), ); }