@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 +20 -12
- package/dist/src/server/ui/app.js +85 -23
- package/dist/src/server/ui/index.html +11 -2
- package/dist/src/server/ui/styles.css +27 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +1 -1
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.
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
**
|
|
389
|
-
|
|
390
|
-
|
|
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
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
-
|
|
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
|
-
|
|
424
|
-
state.selection =
|
|
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
|
|
498
|
-
* l'ensemble des sessions du projet. Filtrer déplie
|
|
499
|
-
*
|
|
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
|
-
|
|
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
|
|
741
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
172
|
-
|
|
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:
|
|
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 {
|
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.23";
|
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.23";
|