This repository has been archived on 2026-10-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
maestro/web/src/projectList.ts
T

77 lines
2.9 KiB
TypeScript

// 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<string, Project[]>();
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),
);
}