// Three-character chip labels for the panel. // // A chip per session only pays off if the label stays put. Two rules do that: // labels are assigned oldest-session-first, so a session starting now takes the // suffixed variant rather than pushing one off the label already on screen; and // a label, once given, belongs to that session until it ends -- even after the // session it was disambiguated against has closed. A label that moves under // your hand is worse than one carrying a digit that no longer looks necessary. // // Imports nothing, so it runs under plain node or gjs. const MAX = 3; /** Split on separators and camelCase humps: "pet-project-server", "outlineMcp". */ function segments(name) { return name .replace(/([a-z0-9])([A-Z])/g, '$1 $2') .split(/[^a-zA-Z0-9]+/) .filter(Boolean); } /** Up to three characters for a project name. * * Initials for multi-segment names, first letters for single words. Initials * matter more than they look: a plain prefix collapses "dev-skills" and * "dev-conventions" onto the same "dev", which is the exact case this has to * keep apart. */ export function abbreviate(name) { const parts = segments(String(name ?? '')); if (!parts.length) return '?'; const raw = parts.length > 1 ? parts.map(p => p[0]).join('') : parts[0]; return raw.slice(0, MAX).toLowerCase(); } /** Assign a label to every session, reusing the ones already handed out. * * `previous` is the mapping from the last run; pass the returned map back in. * Sessions absent from `sessions` drop out, which frees their label for reuse. */ export function assignChips(sessions, previous = new Map()) { const labels = new Map(); const taken = new Set(); for (const session of sessions) { const kept = previous.get(session.sessionId); if (kept !== undefined) { labels.set(session.sessionId, kept); taken.add(kept); } } const fresh = sessions .filter(s => !labels.has(s.sessionId)) .sort((a, b) => (a.since || 0) - (b.since || 0)); for (const session of fresh) { const base = abbreviate(session.base); let label = base; // Digits eat into the base rather than extending past three characters, // so every chip stays the same width and the row does not ripple. for (let n = 2; taken.has(label); n++) { const suffix = String(n); label = base.slice(0, Math.max(1, MAX - suffix.length)) + suffix; } labels.set(session.sessionId, label); taken.add(label); } return labels; }