@imenam/mcp-messenger 1.0.20 → 1.0.22

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
@@ -385,7 +385,8 @@ correspondants et l'état de lecture :
385
385
  la colonne l'écrit donc ainsi. Chaque agent est **replié par défaut** et se déplie d'un
386
386
  chevron sur ses boîtes ; une boîte que son agent n'a pas nommée s'affiche à son adresse,
387
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
388
+ **toutes ses sessions**, cliquer une boîte sur **cette seule session**. Recliquer ne
389
+ relâche rien : on revient à la vue générale par « Tous les agents », en tête de colonne. La colonne se
389
390
  replie sur les seuls avatars d'agents quand la place manque, son bouton de repli restant
390
391
  en place : c'est lui qui la rouvre.
391
392
 
@@ -403,11 +404,15 @@ correspondants et l'état de lecture :
403
404
  - **la liste des fils**, du plus récemment actif au plus ancien, chacun résumé par son
404
405
  nom, ses correspondants, son dernier message et sa priorité la plus haute. Les deux
405
406
  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 ;
407
+ — leurs initiales doivent rester lisibles. **Ces pastilles sont celles des agents, pas
408
+ des boîtes** : dans une liste, ce qu'on cherche du regard est le projet, et deux sessions
409
+ d'un même agent portent donc la même. Ce qui les distingue, le nom de la boîte, se lit au
410
+ survol, avec le projet, l'adresse et la mention de celle qui a écrit en dernier. Le
411
+ lecteur du fil suit la même règle, en tête de chaque message ;
412
+ - **un en-tête qui dit sur quoi la liste est filtrée** : sans filtre elle s'intitule
413
+ « Fils », et dès qu'un agent est choisi c'est lui le sujet — sa pastille en grand, son
414
+ nom en titre, et en dessous ce que le filtre recouvre, toutes ses sessions ou une seule ;
415
+ - **trois puces** au-dessus de la liste : tous, non lus, priorité haute ;
411
416
  - **le lecteur du fil** : la conversation entière dans l'ordre, un bloc par message, avec
412
417
  qui écrit à qui — en clair, pas en adresses — et une pastille disant si le destinataire
413
418
  l'a lu. **Le corps d'un message est rendu comme du markdown** : titres, listes,
@@ -622,7 +627,7 @@ test/ # store, config, API HTTP, outils MCP, binaire réel
622
627
 
623
628
  ### Tests
624
629
 
625
- 266 tests couvrant : les suppressions de boîtes commandées depuis la console — une boîte
630
+ 273 tests couvrant : les suppressions de boîtes commandées depuis la console — une boîte
626
631
  désignée à la main, les vides, celles sans activité depuis une date — avec leur aperçu,
627
632
  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
633
  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;
@@ -672,7 +706,7 @@ function selectionLabel() {
672
706
 
673
707
  function renderThreads(threads) {
674
708
  state.threadCount = threads.length;
675
- el.threadsScope.textContent = `${selectionLabel()} · ${plural(threads.length, "fil")}`;
709
+ renderThreadsHead(threads.length);
676
710
 
677
711
  el.threads.replaceChildren();
678
712
  if (threads.length === 0) {
@@ -685,6 +719,43 @@ function renderThreads(threads) {
685
719
  for (const thread of threads) el.threads.appendChild(threadRow(thread));
686
720
  }
687
721
 
722
+ /**
723
+ * En-tête de la liste : ce sur quoi elle est filtrée, en grand.
724
+ *
725
+ * Sans filtre, elle s'intitule « Fils » et rien d'autre. Dès qu'un agent est choisi, c'est
726
+ * lui le sujet : sa pastille et son nom prennent la place du titre, et la ligne en dessous
727
+ * dit ce que le filtre recouvre — toutes ses sessions, ou une seule.
728
+ */
729
+ function renderThreadsHead(count) {
730
+ const { kind, key } = state.selection;
731
+ const fils = plural(count, "fil");
732
+
733
+ if (kind === "all") {
734
+ el.threadsAvatar.hidden = true;
735
+ el.threadsAvatar.replaceChildren();
736
+ el.threadsName.textContent = "Fils";
737
+ el.threadsScope.textContent = `Toutes les boîtes · ${fils}`;
738
+ return;
739
+ }
740
+
741
+ // Une boîte est d'abord une session de son agent : c'est donc l'agent qui s'affiche,
742
+ // et le nom de la boîte qui précise de quelle session il s'agit.
743
+ const address = kind === "box" ? key : (state.groups.find((g) => g.key === key)?.boxes[0] || {}).id;
744
+ const who = address ? party(address) : null;
745
+ const sessions = kind === "agent" ? (state.groups.find((g) => g.key === key)?.boxes.length ?? 0) : 0;
746
+ const box = kind === "box" ? state.directory.get(key) : null;
747
+
748
+ el.threadsAvatar.hidden = false;
749
+ el.threadsAvatar.replaceChildren(
750
+ address ? agentAvatar(address, "avatar--lg") : avatarFor(key, key, key, "avatar--lg", false)
751
+ );
752
+ el.threadsName.textContent = kind === "agent" ? key : who?.agentName || selectionLabel();
753
+ el.threadsScope.textContent =
754
+ kind === "agent"
755
+ ? `${plural(sessions, "session")} · ${fils}`
756
+ : `${box ? boxLabel(box) : shortAddress(key)} · ${fils}`;
757
+ }
758
+
688
759
  function emptyReason() {
689
760
  if (state.search) return "Aucun fil ne correspond à cette recherche.";
690
761
  if (state.filter === "unread") return "Tout est lu de ce côté.";
@@ -737,17 +808,8 @@ function threadParticipants(thread) {
737
808
 
738
809
  const stack = node("div", "avatar-stack");
739
810
  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);
811
+ const last = id === thread.last_message_from ? "\na écrit le dernier message" : "";
812
+ stack.appendChild(agentAvatar(id, "avatar--sm avatar--stack", `${boxTitle(id)}${last}`));
751
813
  }
752
814
  who.appendChild(stack);
753
815
 
@@ -783,7 +845,9 @@ function renderThread(data) {
783
845
 
784
846
  const meta = node("div", "reader-meta");
785
847
  const stack = node("div", "avatar-stack");
786
- for (const id of participants) stack.appendChild(avatar(id, "avatar--sm avatar--stack"));
848
+ // Le projet d'abord : deux sessions d'un même agent portent la même pastille, le nom
849
+ // de la boîte se lisant au survol.
850
+ for (const id of participants) stack.appendChild(agentAvatar(id, "avatar--sm avatar--stack"));
787
851
  meta.appendChild(stack);
788
852
  meta.appendChild(
789
853
  node(
@@ -814,7 +878,7 @@ function messageRow(message, isLast) {
814
878
  else if (isLast) classes.push("is-last");
815
879
  const row = node("div", classes.join(" "));
816
880
 
817
- row.appendChild(avatar(message.from));
881
+ row.appendChild(agentAvatar(message.from));
818
882
 
819
883
  const column = node("div", "message-body-col");
820
884
  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.22";
@@ -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.22";
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.22",
4
4
  "main": "dist/src/index.js",
5
5
  "scripts": {
6
6
  "test": "npm run build && vitest run",