@imenam/mcp-messenger 1.0.14 → 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 +10 -5
- package/dist/src/server/ui/app.js +73 -11
- package/dist/src/server/ui/styles.css +36 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -347,11 +347,16 @@ correspondants et l'état de lecture :
|
|
|
347
347
|
**toutes ses sessions**, cliquer une boîte sur **cette seule session**. La colonne se
|
|
348
348
|
replie sur les seuls avatars d'agents quand la place manque.
|
|
349
349
|
|
|
350
|
-
**Le classement se fait
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
par
|
|
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 ;
|
|
355
360
|
- **la liste des fils**, du plus récemment actif au plus ancien, chacun résumé par son
|
|
356
361
|
nom, ses correspondants, son dernier message et sa priorité la plus haute. Trois puces
|
|
357
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,23 +289,41 @@ 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, activity: "" });
|
|
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;
|
|
295
298
|
// L'agent hérite de l'activité de sa session la plus vivante.
|
|
296
299
|
if (activityOf(box) > group.activity) group.activity = activityOf(box);
|
|
297
300
|
}
|
|
298
301
|
const list = [...groups.values()];
|
|
299
|
-
for (const group of list) group.boxes.sort(
|
|
302
|
+
for (const group of list) group.boxes.sort(byBox);
|
|
300
303
|
// Le classement est refait ici plutôt que laissé à l'ordre d'arrivée de l'annuaire :
|
|
301
|
-
// ce que la colonne promet
|
|
302
|
-
//
|
|
303
|
-
list.sort(
|
|
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
|
+
);
|
|
304
312
|
return list;
|
|
305
313
|
}
|
|
306
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
|
+
|
|
307
327
|
/** Dernière activité d'une boîte, ou chaîne vide quand elle n'a jamais rien fait. */
|
|
308
328
|
function activityOf(box) {
|
|
309
329
|
return box.last_activity_at || "";
|
|
@@ -320,9 +340,23 @@ function compareActivity(left, right) {
|
|
|
320
340
|
return left < right ? 1 : -1;
|
|
321
341
|
}
|
|
322
342
|
|
|
323
|
-
/**
|
|
324
|
-
function
|
|
325
|
-
|
|
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
|
+
);
|
|
326
360
|
}
|
|
327
361
|
|
|
328
362
|
function isSelected(kind, key) {
|
|
@@ -415,12 +449,17 @@ function groupRow(group) {
|
|
|
415
449
|
)
|
|
416
450
|
);
|
|
417
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
|
+
|
|
418
457
|
const text = node("div", "agent-text");
|
|
419
458
|
text.appendChild(node("div", "agent-name", group.name));
|
|
420
|
-
text.appendChild(node("div", "agent-box",
|
|
459
|
+
text.appendChild(node("div", "agent-box", counts));
|
|
421
460
|
row.appendChild(text);
|
|
422
461
|
if (group.unread > 0) row.appendChild(node("span", "count count--accent", String(group.unread)));
|
|
423
|
-
row.title = `${group.name} · ${
|
|
462
|
+
row.title = `${group.name} · ${counts}`;
|
|
424
463
|
row.addEventListener("click", () => {
|
|
425
464
|
state.expanded.add(group.key);
|
|
426
465
|
select("agent", group.key);
|
|
@@ -430,15 +469,37 @@ function groupRow(group) {
|
|
|
430
469
|
|
|
431
470
|
if (open) {
|
|
432
471
|
const list = node("ul", "box-list");
|
|
433
|
-
|
|
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));
|
|
434
480
|
item.appendChild(list);
|
|
435
481
|
}
|
|
436
482
|
return item;
|
|
437
483
|
}
|
|
438
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
|
+
|
|
439
499
|
function boxRow(box) {
|
|
440
500
|
const item = node("li");
|
|
441
501
|
const classes = ["agent", "box"];
|
|
502
|
+
if (isEmptyBox(box)) classes.push("is-empty");
|
|
442
503
|
if (isSelected("box", box.id)) classes.push("is-selected");
|
|
443
504
|
if (box.unread > 0) classes.push("has-unread");
|
|
444
505
|
if (!box.registered) classes.push("agent--muted");
|
|
@@ -894,6 +955,7 @@ el.logout.addEventListener("click", async () => {
|
|
|
894
955
|
await fetch("/ui/logout", { method: "POST", credentials: "same-origin" }).catch(() => null);
|
|
895
956
|
state.selection = { kind: "all", key: null };
|
|
896
957
|
state.expanded = new Set();
|
|
958
|
+
state.showEmpty = new Set();
|
|
897
959
|
state.threadId = null;
|
|
898
960
|
state.messageId = null;
|
|
899
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;
|
package/dist/src/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const APP_VERSION = "1.0.
|
|
1
|
+
export declare const APP_VERSION = "1.0.15";
|
package/dist/src/version.js
CHANGED
|
@@ -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.
|
|
2
|
+
export const APP_VERSION = "1.0.15";
|