@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 +12 -1
- package/dist/src/server/ui/app.js +107 -13
- 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
|
@@ -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
|
-
|
|
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",
|
|
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} · ${
|
|
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
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
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;
|
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";
|