@imenam/mcp-messenger 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -345,7 +345,18 @@ correspondants et l'état de lecture :
345
345
  chevron sur ses boîtes ; une boîte que son agent n'a pas nommée s'affiche à son adresse,
346
346
  la seule chose qui la distingue alors de ses sœurs. Cliquer l'agent filtre les fils sur
347
347
  **toutes ses sessions**, cliquer une boîte sur **cette seule session**. La colonne se
348
- replie sur les seuls avatars d'agents quand la place manque ;
348
+ replie sur les seuls avatars d'agents quand la place manque.
349
+
350
+ **Le classement se fait en deux temps : d'abord ce qui a servi, ensuite ce qui est
351
+ vide ; puis, dans chacun des deux, la dernière activité, la plus récente en tête.** Une
352
+ boîte est vide tant que rien n'y est passé, ni envoi ni réception — le serveur en alloue
353
+ une par session ouverte, et beaucoup n'échangeront jamais rien. Ces boîtes-là sont
354
+ **masquées par défaut**, derrière un bouton en fin de dépli qui dit combien il y en a ;
355
+ les masquer ne cache jamais de courrier, une boîte sans réception n'ayant par
356
+ construction aucun non-lu. Un agent dont aucune session n'a rien échangé passe de même
357
+ derrière les autres. Chaque boîte affiche la date qui la classe, et deux dates
358
+ identiques sont départagées par l'adresse pour que l'ordre ne bouge pas d'un
359
+ rafraîchissement à l'autre ;
349
360
  - **la liste des fils**, du plus récemment actif au plus ancien, chacun résumé par son
350
361
  nom, ses correspondants, son dernier message et sa priorité la plus haute. Trois puces
351
362
  la filtrent : tous, non lus, priorité haute ;
@@ -33,6 +33,8 @@ const state = {
33
33
  selection: { kind: "all", key: null },
34
34
  /** Noms d'agents dépliés dans la colonne de gauche. Tout est replié au départ. */
35
35
  expanded: new Set(),
36
+ /** Agents dont on a demandé à voir les boîtes vides. Elles sont cachées au départ. */
37
+ showEmpty: new Set(),
36
38
  /** "all" | "unread" | "high" — les puces au-dessus de la liste des fils. */
37
39
  filter: "all",
38
40
  search: "",
@@ -287,13 +289,74 @@ function groupByAgent(boxes) {
287
289
  for (const box of boxes) {
288
290
  const key = box.agent_name || box.id;
289
291
  if (!groups.has(key)) {
290
- groups.set(key, { key, name: box.agent_name || boxLabel(box), boxes: [], unread: 0 });
292
+ groups.set(key, { key, name: box.agent_name || boxLabel(box), boxes: [], unread: 0, filled: 0, activity: "" });
291
293
  }
292
294
  const group = groups.get(key);
293
295
  group.boxes.push(box);
294
296
  group.unread += box.unread;
297
+ if (!isEmptyBox(box)) group.filled += 1;
298
+ // L'agent hérite de l'activité de sa session la plus vivante.
299
+ if (activityOf(box) > group.activity) group.activity = activityOf(box);
295
300
  }
296
- return [...groups.values()];
301
+ const list = [...groups.values()];
302
+ for (const group of list) group.boxes.sort(byBox);
303
+ // Le classement est refait ici plutôt que laissé à l'ordre d'arrivée de l'annuaire :
304
+ // ce que la colonne promet ne doit pas dépendre du hasard d'un tri fait ailleurs. Un
305
+ // agent dont aucune session n'a rien échangé passe derrière les autres, comme ses boîtes.
306
+ list.sort(
307
+ (a, b) =>
308
+ compareEmpty(a.filled === 0, b.filled === 0) ||
309
+ compareActivity(a.activity, b.activity) ||
310
+ (a.key < b.key ? -1 : a.key > b.key ? 1 : 0)
311
+ );
312
+ return list;
313
+ }
314
+
315
+ /**
316
+ * Une boîte est vide tant que rien n'y est passé, ni envoi ni réception.
317
+ *
318
+ * Le serveur alloue une boîte à chaque session ouverte : beaucoup n'échangeront jamais
319
+ * rien et n'ont donc rien à dire à qui supervise. Une boîte vide n'a par construction
320
+ * aucun non-lu — `received` à zéro l'implique — la masquer ne cache donc jamais de
321
+ * courrier en attente.
322
+ */
323
+ function isEmptyBox(box) {
324
+ return box.sent + box.received === 0;
325
+ }
326
+
327
+ /** Dernière activité d'une boîte, ou chaîne vide quand elle n'a jamais rien fait. */
328
+ function activityOf(box) {
329
+ return box.last_activity_at || "";
330
+ }
331
+
332
+ /**
333
+ * Compare deux dates ISO, la plus récente d'abord.
334
+ *
335
+ * Les dates ISO se comparent comme du texte, et la chaîne vide — une boîte qui n'a jamais
336
+ * vécu — est plus petite que toute date : elle finit donc en queue, sans cas particulier.
337
+ */
338
+ function compareActivity(left, right) {
339
+ if (left === right) return 0;
340
+ return left < right ? 1 : -1;
341
+ }
342
+
343
+ /** Le plein avant le vide. Premier critère du classement, avant même la date. */
344
+ function compareEmpty(left, right) {
345
+ if (left === right) return 0;
346
+ return left ? 1 : -1;
347
+ }
348
+
349
+ /**
350
+ * Deux classements emboîtés : d'abord les boîtes qui ont servi, puis les vides ; dans
351
+ * chacun des deux, la plus récemment active en tête. À égalité, l'adresse départage, pour
352
+ * que l'ordre ne bouge pas d'un rafraîchissement à l'autre.
353
+ */
354
+ function byBox(a, b) {
355
+ return (
356
+ compareEmpty(isEmptyBox(a), isEmptyBox(b)) ||
357
+ compareActivity(activityOf(a), activityOf(b)) ||
358
+ (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
359
+ );
297
360
  }
298
361
 
299
362
  function isSelected(kind, key) {
@@ -386,12 +449,17 @@ function groupRow(group) {
386
449
  )
387
450
  );
388
451
 
452
+ const empty = group.boxes.length - group.filled;
453
+ // Le décompte annonce ce que le dépli va montrer : sans quoi « 5 boîtes » suivi de deux
454
+ // lignes se lirait comme un bug plutôt que comme des boîtes vides mises de côté.
455
+ const counts = empty > 0 ? `${plural(group.filled, "boîte")} · ${plural(empty, "vide")}` : plural(group.filled, "boîte");
456
+
389
457
  const text = node("div", "agent-text");
390
458
  text.appendChild(node("div", "agent-name", group.name));
391
- text.appendChild(node("div", "agent-box", plural(group.boxes.length, "boîte")));
459
+ text.appendChild(node("div", "agent-box", counts));
392
460
  row.appendChild(text);
393
461
  if (group.unread > 0) row.appendChild(node("span", "count count--accent", String(group.unread)));
394
- row.title = `${group.name} · ${plural(group.boxes.length, "boîte")}`;
462
+ row.title = `${group.name} · ${counts}`;
395
463
  row.addEventListener("click", () => {
396
464
  state.expanded.add(group.key);
397
465
  select("agent", group.key);
@@ -401,15 +469,37 @@ function groupRow(group) {
401
469
 
402
470
  if (open) {
403
471
  const list = node("ul", "box-list");
404
- for (const box of group.boxes) list.appendChild(boxRow(box));
472
+ // Les vides sont derrière dans `group.boxes` : les montrer, c'est allonger la liste,
473
+ // jamais en réordonner le début.
474
+ const revealed = state.showEmpty.has(group.key);
475
+ for (const box of group.boxes) {
476
+ if (isEmptyBox(box) && !revealed) continue;
477
+ list.appendChild(boxRow(box));
478
+ }
479
+ if (empty > 0) list.appendChild(emptyToggleRow(group, empty, revealed));
405
480
  item.appendChild(list);
406
481
  }
407
482
  return item;
408
483
  }
409
484
 
485
+ /** Dernière ligne du dépli : ouvre ou referme les boîtes qui n'ont jamais rien échangé. */
486
+ function emptyToggleRow(group, empty, revealed) {
487
+ const item = node("li");
488
+ const what = empty > 1 ? `les ${empty} boîtes vides` : "la boîte vide";
489
+ const toggle = button("box-empty-toggle", `${revealed ? "Masquer" : "Afficher"} ${what}`, () => {
490
+ if (revealed) state.showEmpty.delete(group.key);
491
+ else state.showEmpty.add(group.key);
492
+ renderAgents({ agents: [...state.directory.values()], threads: currentThreadCount() });
493
+ });
494
+ toggle.setAttribute("aria-expanded", revealed ? "true" : "false");
495
+ item.appendChild(toggle);
496
+ return item;
497
+ }
498
+
410
499
  function boxRow(box) {
411
500
  const item = node("li");
412
501
  const classes = ["agent", "box"];
502
+ if (isEmptyBox(box)) classes.push("is-empty");
413
503
  if (isSelected("box", box.id)) classes.push("is-selected");
414
504
  if (box.unread > 0) classes.push("has-unread");
415
505
  if (!box.registered) classes.push("agent--muted");
@@ -425,17 +515,20 @@ function boxRow(box) {
425
515
  const name = node("div", "agent-name", label);
426
516
  if (!box.display_name) name.classList.add("mono", "is-absent");
427
517
  text.appendChild(name);
428
- text.appendChild(
429
- node(
430
- "div",
431
- "agent-box",
432
- box.registered ? `${plural(box.sent, "envoyé")} · ${plural(box.received, "reçu")}` : "jamais connectée"
433
- )
434
- );
518
+ // Les sessions d'un agent sont classées sur cette date : sans elle, leur ordre
519
+ // paraîtrait arbitraire. Les compteurs passent dans l'info-bulle, faute de place.
520
+ const activity = box.last_activity_at
521
+ ? `activité ${formatShort(box.last_activity_at)}`
522
+ : box.registered
523
+ ? "aucune activité"
524
+ : "jamais connectée";
525
+ const meta = node("div", "agent-box", activity);
526
+ if (!box.last_activity_at) meta.classList.add("is-absent");
527
+ text.appendChild(meta);
435
528
  row.appendChild(text);
436
529
 
437
530
  if (box.unread > 0) row.appendChild(node("span", "count count--accent", String(box.unread)));
438
- row.title = box.id;
531
+ row.title = `${box.id}\n${plural(box.sent, "envoyé")} · ${plural(box.received, "reçu")}`;
439
532
  row.addEventListener("click", () => select("box", box.id));
440
533
  item.appendChild(row);
441
534
  return item;
@@ -862,6 +955,7 @@ el.logout.addEventListener("click", async () => {
862
955
  await fetch("/ui/logout", { method: "POST", credentials: "same-origin" }).catch(() => null);
863
956
  state.selection = { kind: "all", key: null };
864
957
  state.expanded = new Set();
958
+ state.showEmpty = new Set();
865
959
  state.threadId = null;
866
960
  state.messageId = null;
867
961
  state.directory = new Map();
@@ -577,6 +577,42 @@ body {
577
577
  font-size: 11px;
578
578
  }
579
579
 
580
+ /* Les boîtes vides sont repliées derrière ce bouton : le lien ferme la liste plutôt que
581
+ d'aligner des sessions qui n'ont jamais rien échangé. */
582
+ .box-empty-toggle {
583
+ display: block;
584
+ width: 100%;
585
+ padding: 6px 10px;
586
+ border: 0;
587
+ border-radius: var(--radius);
588
+ background: none;
589
+ font: inherit;
590
+ font-size: 12px;
591
+ color: var(--muted);
592
+ text-align: left;
593
+ cursor: pointer;
594
+ }
595
+
596
+ .box-empty-toggle:hover {
597
+ background: var(--border-soft);
598
+ color: var(--accent);
599
+ }
600
+
601
+ .box-empty-toggle:focus-visible {
602
+ outline: 2px solid var(--accent);
603
+ outline-offset: -2px;
604
+ }
605
+
606
+ /* Une boîte vide reste lisible mais s'efface : elle est là pour être retrouvée, pas lue. */
607
+ .box.is-empty .agent-name {
608
+ color: var(--muted);
609
+ font-weight: 400;
610
+ }
611
+
612
+ .box.is-empty .avatar {
613
+ opacity: 0.5;
614
+ }
615
+
580
616
  .agents-foot {
581
617
  padding: 16px 20px;
582
618
  font-size: 12px;
@@ -1 +1 @@
1
- export declare const APP_VERSION = "1.0.13";
1
+ export declare const APP_VERSION = "1.0.15";
@@ -1,2 +1,2 @@
1
1
  // Généré par scripts/generate-version.js — ne pas modifier à la main.
2
- export const APP_VERSION = "1.0.13";
2
+ export const APP_VERSION = "1.0.15";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/mcp-messenger",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "main": "dist/src/index.js",
5
5
  "scripts": {
6
6
  "test": "npm run build && vitest run",