@imenam/mcp-messenger 1.0.20 → 1.0.23

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
@@ -382,12 +382,16 @@ organisés autour du fil — l'unité d'une correspondance, celle qui porte le s
382
382
  correspondants et l'état de lecture :
383
383
 
384
384
  - **une colonne d'agents à deux niveaux** : un agent tient une boîte par session ouverte,
385
- la colonne l'écrit donc ainsi. Chaque agent est **replié par défaut** et se déplie d'un
386
- chevron sur ses boîtes ; une boîte que son agent n'a pas nommée s'affiche à son adresse,
387
- la seule chose qui la distingue alors de ses sœurs. Cliquer l'agent filtre les fils sur
388
- **toutes ses sessions**, cliquer une boîte sur **cette seule session**. La colonne se
389
- replie sur les seuls avatars d'agents quand la place manque, son bouton de repli restant
390
- en place : c'est lui qui la rouvre.
385
+ la colonne l'écrit donc ainsi. Une boîte que son agent n'a pas nommée s'affiche à son
386
+ adresse, la seule chose qui la distingue alors de ses sœurs.
387
+
388
+ **Deux gestes, deux effets, sans recouvrement.** Cliquer le nom d'un agent filtre les
389
+ fils sur **toutes ses sessions**, cliquer une boîte sur **cette seule session** ; le
390
+ chevron, lui, déplie ou replie les boîtes sans toucher au filtre. Chaque agent est
391
+ **replié par défaut**, et sélectionner un agent n'ouvre rien. Recliquer ne relâche rien
392
+ non plus : on revient à la vue générale par « Tous les agents », en tête de colonne. La
393
+ colonne se replie sur les seuls avatars d'agents quand la place manque, son bouton de
394
+ repli restant en place : c'est lui qui la rouvre.
391
395
 
392
396
  **Le classement se fait en deux temps : d'abord ce qui a servi, ensuite ce qui est
393
397
  vide ; puis, dans chacun des deux, la dernière activité, la plus récente en tête.** Une
@@ -403,11 +407,15 @@ correspondants et l'état de lecture :
403
407
  - **la liste des fils**, du plus récemment actif au plus ancien, chacun résumé par son
404
408
  nom, ses correspondants, son dernier message et sa priorité la plus haute. Les deux
405
409
  correspondants y sont d'abord **deux pastilles côte à côte**, séparées et non superposées
406
- — leurs initiales doivent rester lisibles —, de la couleur que leur adresse leur donne
407
- partout ailleurs : on voit qui parle avant de lire. Au survol, la
408
- pastille dit d'où la boîte écrit, ce dont la session s'occupe, son adresse, et si c'est
409
- elle qui a écrit en dernier. Trois puces
410
- la filtrent : tous, non lus, priorité haute ;
410
+ — leurs initiales doivent rester lisibles. **Ces pastilles sont celles des agents, pas
411
+ des boîtes** : dans une liste, ce qu'on cherche du regard est le projet, et deux sessions
412
+ d'un même agent portent donc la même. Ce qui les distingue, le nom de la boîte, se lit au
413
+ survol, avec le projet, l'adresse et la mention de celle qui a écrit en dernier. Le
414
+ lecteur du fil suit la même règle, en tête de chaque message ;
415
+ - **un en-tête qui dit sur quoi la liste est filtrée** : sans filtre elle s'intitule
416
+ « Fils », et dès qu'un agent est choisi c'est lui le sujet — sa pastille en grand, son
417
+ nom en titre, et en dessous ce que le filtre recouvre, toutes ses sessions ou une seule ;
418
+ - **trois puces** au-dessus de la liste : tous, non lus, priorité haute ;
411
419
  - **le lecteur du fil** : la conversation entière dans l'ordre, un bloc par message, avec
412
420
  qui écrit à qui — en clair, pas en adresses — et une pastille disant si le destinataire
413
421
  l'a lu. **Le corps d'un message est rendu comme du markdown** : titres, listes,
@@ -622,7 +630,7 @@ test/ # store, config, API HTTP, outils MCP, binaire réel
622
630
 
623
631
  ### Tests
624
632
 
625
- 266 tests couvrant : les suppressions de boîtes commandées depuis la console — une boîte
633
+ 274 tests couvrant : les suppressions de boîtes commandées depuis la console — une boîte
626
634
  désignée à la main, les vides, celles sans activité depuis une date — avec leur aperçu,
627
635
  leur double confirmation et le refus nominatif d'une boîte qui ne répond plus au critère, la reprise de la boîte d'une session interrompue (clé de session de
628
636
  l'hôte, boîte orpheline, boîte d'une session vivante jamais reprise), le rendu markdown des messages et l'aplatissement des aperçus (dont
@@ -82,6 +82,8 @@ const el = {
82
82
  agents: document.getElementById("agents"),
83
83
  agentsFoot: document.getElementById("agents-foot"),
84
84
  collapse: document.getElementById("collapse"),
85
+ threadsAvatar: document.getElementById("threads-avatar"),
86
+ threadsName: document.getElementById("threads-name"),
85
87
  threadsScope: document.getElementById("threads-scope"),
86
88
  // Le panneau, pas la liste qu'il contient : c'est lui que le séparateur redimensionne.
87
89
  threadsPane: document.querySelector(".threads"),
@@ -235,6 +237,31 @@ function avatar(agentId, extraClass) {
235
237
  return avatarFor(agentId, label, title, extraClass, !who.registered);
236
238
  }
237
239
 
240
+ /**
241
+ * Pastille de l'agent, et non de la boîte.
242
+ *
243
+ * Dans une liste, ce qui se cherche du regard est le projet : deux sessions d'un même
244
+ * agent portent donc la même pastille, et c'est voulu — ce qui les distingue, le nom de
245
+ * la boîte, se lit au survol. La pastille de boîte reste celle du lecteur et du volet,
246
+ * là où l'on regarde une session en particulier.
247
+ */
248
+ function agentAvatar(agentId, extraClass, title) {
249
+ const who = party(agentId);
250
+ const seed = who.agentName || agentId;
251
+ const label = who.agentName || who.box || shortAddress(agentId);
252
+ return avatarFor(seed, label, title ?? boxTitle(agentId), extraClass, !who.registered);
253
+ }
254
+
255
+ /** Ce qu'une pastille dit au survol : le projet, la session, l'adresse. */
256
+ function boxTitle(agentId) {
257
+ const who = party(agentId);
258
+ return [
259
+ who.agentName || "agent sans nom",
260
+ who.box || (who.registered ? "boîte sans nom" : "boîte jamais connectée"),
261
+ agentId,
262
+ ].join("\n");
263
+ }
264
+
238
265
  // --- Dates -----------------------------------------------------------------
239
266
 
240
267
  function formatDate(iso) {
@@ -419,9 +446,16 @@ function isSelected(kind, key) {
419
446
  return state.selection.kind === kind && state.selection.key === key;
420
447
  }
421
448
 
449
+ /**
450
+ * Choisit ce que la liste des fils montre.
451
+ *
452
+ * Recliquer sur la sélection courante ne la relâche pas : cliquer un agent veut dire
453
+ * « montre-moi cet agent », et le redire ne peut pas vouloir dire le contraire. On revient
454
+ * à la vue générale par « Tous les agents », en tête de colonne, là où on l'attend.
455
+ */
422
456
  function select(kind, key) {
423
- // Recliquer sur la sélection courante la relâche : le filtre ne s'emprisonne pas.
424
- state.selection = isSelected(kind, key) ? { kind: "all", key: null } : { kind, key };
457
+ if (isSelected(kind, key)) return;
458
+ state.selection = { kind, key };
425
459
  state.threadId = null;
426
460
  state.messageId = null;
427
461
  el.sheet.hidden = true;
@@ -494,9 +528,10 @@ function allRow(overview) {
494
528
  /**
495
529
  * Un agent et, dépliées sous lui, ses boîtes.
496
530
  *
497
- * Deux zones de clic, deux gestes distincts : le chevron ouvre ou ferme, le nom filtre sur
498
- * l'ensemble des sessions du projet. Filtrer déplie au passage, pour que la sélection
499
- * montre toujours ce qu'elle recouvre.
531
+ * Deux zones de clic, deux gestes distincts et sans effet l'un sur l'autre : le chevron
532
+ * ouvre ou ferme, le nom filtre sur l'ensemble des sessions du projet. Filtrer ne déplie
533
+ * pas — un clic ne doit faire que ce qu'il annonce, et voir les sessions d'un agent est
534
+ * une autre question que ne lire que ses fils.
500
535
  */
501
536
  function groupRow(group) {
502
537
  const item = node("li", "agent-group");
@@ -543,10 +578,7 @@ function groupRow(group) {
543
578
  row.appendChild(text);
544
579
  if (group.unread > 0) row.appendChild(node("span", "count count--accent", String(group.unread)));
545
580
  row.title = `${group.name} · ${counts}`;
546
- row.addEventListener("click", () => {
547
- state.expanded.add(group.key);
548
- select("agent", group.key);
549
- });
581
+ row.addEventListener("click", () => select("agent", group.key));
550
582
  header.appendChild(row);
551
583
  item.appendChild(header);
552
584
 
@@ -672,7 +704,7 @@ function selectionLabel() {
672
704
 
673
705
  function renderThreads(threads) {
674
706
  state.threadCount = threads.length;
675
- el.threadsScope.textContent = `${selectionLabel()} · ${plural(threads.length, "fil")}`;
707
+ renderThreadsHead(threads.length);
676
708
 
677
709
  el.threads.replaceChildren();
678
710
  if (threads.length === 0) {
@@ -685,6 +717,43 @@ function renderThreads(threads) {
685
717
  for (const thread of threads) el.threads.appendChild(threadRow(thread));
686
718
  }
687
719
 
720
+ /**
721
+ * En-tête de la liste : ce sur quoi elle est filtrée, en grand.
722
+ *
723
+ * Sans filtre, elle s'intitule « Fils » et rien d'autre. Dès qu'un agent est choisi, c'est
724
+ * lui le sujet : sa pastille et son nom prennent la place du titre, et la ligne en dessous
725
+ * dit ce que le filtre recouvre — toutes ses sessions, ou une seule.
726
+ */
727
+ function renderThreadsHead(count) {
728
+ const { kind, key } = state.selection;
729
+ const fils = plural(count, "fil");
730
+
731
+ if (kind === "all") {
732
+ el.threadsAvatar.hidden = true;
733
+ el.threadsAvatar.replaceChildren();
734
+ el.threadsName.textContent = "Fils";
735
+ el.threadsScope.textContent = `Toutes les boîtes · ${fils}`;
736
+ return;
737
+ }
738
+
739
+ // Une boîte est d'abord une session de son agent : c'est donc l'agent qui s'affiche,
740
+ // et le nom de la boîte qui précise de quelle session il s'agit.
741
+ const address = kind === "box" ? key : (state.groups.find((g) => g.key === key)?.boxes[0] || {}).id;
742
+ const who = address ? party(address) : null;
743
+ const sessions = kind === "agent" ? (state.groups.find((g) => g.key === key)?.boxes.length ?? 0) : 0;
744
+ const box = kind === "box" ? state.directory.get(key) : null;
745
+
746
+ el.threadsAvatar.hidden = false;
747
+ el.threadsAvatar.replaceChildren(
748
+ address ? agentAvatar(address, "avatar--lg") : avatarFor(key, key, key, "avatar--lg", false)
749
+ );
750
+ el.threadsName.textContent = kind === "agent" ? key : who?.agentName || selectionLabel();
751
+ el.threadsScope.textContent =
752
+ kind === "agent"
753
+ ? `${plural(sessions, "session")} · ${fils}`
754
+ : `${box ? boxLabel(box) : shortAddress(key)} · ${fils}`;
755
+ }
756
+
688
757
  function emptyReason() {
689
758
  if (state.search) return "Aucun fil ne correspond à cette recherche.";
690
759
  if (state.filter === "unread") return "Tout est lu de ce côté.";
@@ -737,17 +806,8 @@ function threadParticipants(thread) {
737
806
 
738
807
  const stack = node("div", "avatar-stack");
739
808
  for (const id of thread.participants) {
740
- const person = party(id);
741
- const badge = avatar(id, "avatar--sm avatar--stack");
742
- badge.title = [
743
- person.agentName || "agent sans nom",
744
- person.box || (person.registered ? "boîte sans nom" : "boîte jamais connectée"),
745
- id,
746
- id === thread.last_message_from ? "a écrit le dernier message" : null,
747
- ]
748
- .filter(Boolean)
749
- .join("\n");
750
- stack.appendChild(badge);
809
+ const last = id === thread.last_message_from ? "\na écrit le dernier message" : "";
810
+ stack.appendChild(agentAvatar(id, "avatar--sm avatar--stack", `${boxTitle(id)}${last}`));
751
811
  }
752
812
  who.appendChild(stack);
753
813
 
@@ -783,7 +843,9 @@ function renderThread(data) {
783
843
 
784
844
  const meta = node("div", "reader-meta");
785
845
  const stack = node("div", "avatar-stack");
786
- for (const id of participants) stack.appendChild(avatar(id, "avatar--sm avatar--stack"));
846
+ // Le projet d'abord : deux sessions d'un même agent portent la même pastille, le nom
847
+ // de la boîte se lisant au survol.
848
+ for (const id of participants) stack.appendChild(agentAvatar(id, "avatar--sm avatar--stack"));
787
849
  meta.appendChild(stack);
788
850
  meta.appendChild(
789
851
  node(
@@ -814,7 +876,7 @@ function messageRow(message, isLast) {
814
876
  else if (isLast) classes.push("is-last");
815
877
  const row = node("div", classes.join(" "));
816
878
 
817
- row.appendChild(avatar(message.from));
879
+ row.appendChild(agentAvatar(message.from));
818
880
 
819
881
  const column = node("div", "message-body-col");
820
882
  const head = node("div", "message-head");
@@ -168,8 +168,17 @@
168
168
 
169
169
  <section class="threads">
170
170
  <div class="threads-head">
171
- <h2>Fils</h2>
172
- <p class="muted" id="threads-scope"></p>
171
+ <!--
172
+ L'en-tête dit sur quoi la liste est filtrée. Quand c'est un agent, il y tient la
173
+ place principale, pastille comprise : c'est le sujet de ce qu'on lit en dessous.
174
+ -->
175
+ <div class="threads-title">
176
+ <span class="threads-avatar" id="threads-avatar" hidden></span>
177
+ <div class="threads-heading">
178
+ <h2 id="threads-name">Fils</h2>
179
+ <p class="muted" id="threads-scope"></p>
180
+ </div>
181
+ </div>
173
182
  <div class="chips" id="chips">
174
183
  <button type="button" class="chip is-active" data-filter="all">Tous</button>
175
184
  <button type="button" class="chip" data-filter="unread">Non lus</button>
@@ -1014,14 +1014,40 @@ body {
1014
1014
  padding: 16px 20px 12px;
1015
1015
  }
1016
1016
 
1017
+ /* L'agent filtré est le sujet de la liste : sa pastille et son nom ouvrent le panneau. */
1018
+ .threads-title {
1019
+ display: flex;
1020
+ align-items: center;
1021
+ gap: 12px;
1022
+ min-width: 0;
1023
+ }
1024
+
1025
+ .threads-heading {
1026
+ min-width: 0;
1027
+ }
1028
+
1017
1029
  .threads-head h2 {
1018
1030
  margin: 0;
1019
- font-size: 15px;
1031
+ font-size: 17px;
1032
+ line-height: 1.25;
1033
+ white-space: nowrap;
1034
+ overflow: hidden;
1035
+ text-overflow: ellipsis;
1020
1036
  }
1021
1037
 
1022
1038
  .threads-head p {
1023
1039
  margin: 2px 0 0;
1024
1040
  font-size: 12px;
1041
+ white-space: nowrap;
1042
+ overflow: hidden;
1043
+ text-overflow: ellipsis;
1044
+ }
1045
+
1046
+ .avatar--lg {
1047
+ width: 40px;
1048
+ height: 40px;
1049
+ font-size: 15px;
1050
+ flex: 0 0 auto;
1025
1051
  }
1026
1052
 
1027
1053
  .chips {
@@ -1 +1 @@
1
- export declare const APP_VERSION = "1.0.20";
1
+ export declare const APP_VERSION = "1.0.23";
@@ -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.20";
2
+ export const APP_VERSION = "1.0.23";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/mcp-messenger",
3
- "version": "1.0.20",
3
+ "version": "1.0.23",
4
4
  "main": "dist/src/index.js",
5
5
  "scripts": {
6
6
  "test": "npm run build && vitest run",