The panel showed a single aggregate: the most urgent session, plus a "+N" for the others. That answers "what is the worst thing happening", which is not the question with five projects open -- "what is each of them doing" needs each of them on screen. Each session now gets a chip: state glyph plus a three-character project label, in the centre box just right of the clock. Time in state stays on the first chip only; five counters side by side are a row of numbers, not an answer. Labels are initials for multi-segment names, first letters otherwise. Initials rather than a prefix, because a prefix collapses dev-skills and dev-conventions onto the same "dev" -- exactly the pair that has to stay apart. Collisions, including two sessions in one project where the cwd is identical, take a digit: ds, ds2, ds3. Two rules keep a label still, and the feature is worthless without them. Assignment runs oldest-session-first, so a session starting now takes the suffix instead of displacing one already on screen; and a label belongs to its session until it ends, even after whatever forced the digit has closed. A label that moves under your hand is worse than one carrying a digit that no longer looks necessary. The zellij tab name was considered as the label source and dropped. It arrives asynchronously from dump-layout, so a sticky label would freeze whatever the project name produced first and never adopt it. Digits already separate same-project sessions, so the tab name buys nothing here and stays where it is useful, in the menu. Menu rows now lead with the same label, so the mapping from "ds" to dev-skills is read rather than guessed.
74 lines
2.6 KiB
JavaScript
74 lines
2.6 KiB
JavaScript
// 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;
|
|
}
|