@imenam/mcp-messenger 1.0.22 → 1.0.24
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 -9
- package/dist/src/server/ui/app.js +84 -25
- package/dist/src/server/ui/styles.css +116 -16
- 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,13 +382,18 @@ 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
|
-
|
|
391
|
-
|
|
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. **Repliée, elle garde deux signes
|
|
395
|
+
distincts** : l'anneau désigne l'agent sélectionné, la pastille bleue dit qu'il reste du
|
|
396
|
+
non-lu — la même convention que le point de la liste des fils.
|
|
392
397
|
|
|
393
398
|
**Le classement se fait en deux temps : d'abord ce qui a servi, ensuite ce qui est
|
|
394
399
|
vide ; puis, dans chacun des deux, la dernière activité, la plus récente en tête.** Une
|
|
@@ -415,7 +420,13 @@ correspondants et l'état de lecture :
|
|
|
415
420
|
- **trois puces** au-dessus de la liste : tous, non lus, priorité haute ;
|
|
416
421
|
- **le lecteur du fil** : la conversation entière dans l'ordre, un bloc par message, avec
|
|
417
422
|
qui écrit à qui — en clair, pas en adresses — et une pastille disant si le destinataire
|
|
418
|
-
l'a lu. **
|
|
423
|
+
l'a lu. **Les messages s'ouvrent repliés** : chacun tient alors sur sa ligne d'identité —
|
|
424
|
+
pastille, qui écrit à qui, heure — et sur **l'objet du message**, qui est ce qui
|
|
425
|
+
l'identifie. Les deux forment le bandeau qui l'ouvre et le referme, cliquable d'un bout à
|
|
426
|
+
l'autre et grisé au survol pour se donner à voir comme une cible. Ouvrir un message ne
|
|
427
|
+
change rien à ce bandeau : le corps vient simplement se poser dessous.
|
|
428
|
+
Un fil se parcourt avant de se lire, et le rafraîchissement automatique ne referme pas
|
|
429
|
+
ce qui est en train d'être lu. **Le corps d'un message est rendu comme du markdown** : titres, listes,
|
|
419
430
|
citations, tableaux, blocs de code, emphase et liens. C'est la langue dans laquelle les
|
|
420
431
|
agents s'écrivent, la console la lit donc ainsi plutôt que d'afficher sa syntaxe. Le
|
|
421
432
|
rendu construit des nœuds un par un et n'insère jamais de HTML : rien de ce qu'un agent
|
|
@@ -627,7 +638,7 @@ test/ # store, config, API HTTP, outils MCP, binaire réel
|
|
|
627
638
|
|
|
628
639
|
### Tests
|
|
629
640
|
|
|
630
|
-
|
|
641
|
+
284 tests couvrant : les suppressions de boîtes commandées depuis la console — une boîte
|
|
631
642
|
désignée à la main, les vides, celles sans activité depuis une date — avec leur aperçu,
|
|
632
643
|
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
|
|
633
644
|
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
|
|
@@ -54,6 +54,11 @@ const state = {
|
|
|
54
54
|
threadId: null,
|
|
55
55
|
/** Message dont le volet de détail est ouvert, ou null si le volet est fermé. */
|
|
56
56
|
messageId: null,
|
|
57
|
+
/**
|
|
58
|
+
* Messages dépliés dans le lecteur, par identifiant. Vide au départ : un fil s'ouvre
|
|
59
|
+
* replié, et se parcourt avant de se lire.
|
|
60
|
+
*/
|
|
61
|
+
openMessages: new Set(),
|
|
57
62
|
collapsed: false,
|
|
58
63
|
/** Annuaire des boîtes, indexé par adresse : rempli par chaque `/admin/overview`. */
|
|
59
64
|
directory: new Map(),
|
|
@@ -528,9 +533,10 @@ function allRow(overview) {
|
|
|
528
533
|
/**
|
|
529
534
|
* Un agent et, dépliées sous lui, ses boîtes.
|
|
530
535
|
*
|
|
531
|
-
* Deux zones de clic, deux gestes distincts
|
|
532
|
-
* l'ensemble des sessions du projet. Filtrer déplie
|
|
533
|
-
*
|
|
536
|
+
* Deux zones de clic, deux gestes distincts et sans effet l'un sur l'autre : le chevron
|
|
537
|
+
* ouvre ou ferme, le nom filtre sur l'ensemble des sessions du projet. Filtrer ne déplie
|
|
538
|
+
* pas — un clic ne doit faire que ce qu'il annonce, et voir les sessions d'un agent est
|
|
539
|
+
* une autre question que ne lire que ses fils.
|
|
534
540
|
*/
|
|
535
541
|
function groupRow(group) {
|
|
536
542
|
const item = node("li", "agent-group");
|
|
@@ -577,10 +583,7 @@ function groupRow(group) {
|
|
|
577
583
|
row.appendChild(text);
|
|
578
584
|
if (group.unread > 0) row.appendChild(node("span", "count count--accent", String(group.unread)));
|
|
579
585
|
row.title = `${group.name} · ${counts}`;
|
|
580
|
-
row.addEventListener("click", () =>
|
|
581
|
-
state.expanded.add(group.key);
|
|
582
|
-
select("agent", group.key);
|
|
583
|
-
});
|
|
586
|
+
row.addEventListener("click", () => select("agent", group.key));
|
|
584
587
|
header.appendChild(row);
|
|
585
588
|
item.appendChild(header);
|
|
586
589
|
|
|
@@ -871,29 +874,82 @@ function renderThread(data) {
|
|
|
871
874
|
el.reader.replaceChildren(container);
|
|
872
875
|
}
|
|
873
876
|
|
|
877
|
+
/**
|
|
878
|
+
* Un message du fil, replié ou déplié.
|
|
879
|
+
*
|
|
880
|
+
* Un fil se relit d'abord de haut en bas : ce qui doit sauter aux yeux est qui a écrit,
|
|
881
|
+
* quand, et sur quoi. Les corps sont donc repliés au départ, chaque message tenant sur sa
|
|
882
|
+
* ligne de titre, et c'est cette ligne qui l'ouvre. L'état est retenu par identifiant de
|
|
883
|
+
* message : le rafraîchissement automatique ne referme donc pas ce qu'on est en train de
|
|
884
|
+
* lire.
|
|
885
|
+
*/
|
|
874
886
|
function messageRow(message, isLast) {
|
|
875
887
|
const item = node("li");
|
|
888
|
+
fillMessage(item, message, isLast);
|
|
889
|
+
return item;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Remplit la ligne d'un message, dépliée ou non.
|
|
894
|
+
*
|
|
895
|
+
* Le nœud est donné plutôt que rendu : ouvrir un message ne redessine que lui, et c'est
|
|
896
|
+
* bien cette ligne-là, celle qui est dans la page, qu'il faut réécrire — en reconstruire
|
|
897
|
+
* une autre reviendrait à modifier un élément que personne ne regarde.
|
|
898
|
+
*/
|
|
899
|
+
function fillMessage(item, message, isLast) {
|
|
900
|
+
const open = state.openMessages.has(message.message_id);
|
|
876
901
|
const classes = ["message"];
|
|
902
|
+
if (open) classes.push("is-expanded");
|
|
877
903
|
if (state.messageId === message.message_id) classes.push("is-open");
|
|
878
904
|
else if (isLast) classes.push("is-last");
|
|
879
905
|
const row = node("div", classes.join(" "));
|
|
906
|
+
const column = node("div", "message-body-col");
|
|
907
|
+
|
|
908
|
+
/*
|
|
909
|
+
Tout ce qui est visible d'un message replié est cliquable, pastille comprise : le
|
|
910
|
+
bandeau est le bouton, et le survol le grise pour qu'on le voie comme tel. Pas de
|
|
911
|
+
chevron — sur une ligne qui s'annonce déjà comme une cible, il n'apprend rien.
|
|
912
|
+
*/
|
|
913
|
+
const head = node("button", "message-head");
|
|
914
|
+
head.type = "button";
|
|
915
|
+
head.setAttribute("aria-expanded", open ? "true" : "false");
|
|
916
|
+
head.appendChild(agentAvatar(message.from));
|
|
917
|
+
|
|
918
|
+
const identity = node("span", "message-identity");
|
|
919
|
+
identity.appendChild(node("span", "message-from", party(message.from).name));
|
|
920
|
+
identity.appendChild(node("span", "message-to", `→ ${party(message.to).name}`));
|
|
921
|
+
if (message.priority === "high") identity.appendChild(tag("tag tag--high", "priorité haute"));
|
|
922
|
+
if (message.priority === "low") identity.appendChild(tag("tag", "priorité basse"));
|
|
923
|
+
if (!message.read) {
|
|
924
|
+
const dot = node("span", "message-dot");
|
|
925
|
+
dot.title = `non lu par ${party(message.to).name}`;
|
|
926
|
+
identity.appendChild(dot);
|
|
927
|
+
}
|
|
928
|
+
identity.appendChild(node("span", "message-time", formatShort(message.created_at)));
|
|
929
|
+
head.appendChild(identity);
|
|
880
930
|
|
|
881
|
-
|
|
931
|
+
// L'objet, toujours : c'est le titre du message, et c'est lui qu'on cherche du regard.
|
|
932
|
+
// Le bandeau ne bouge donc pas quand le message s'ouvre, seul le corps vient dessous.
|
|
933
|
+
head.appendChild(node("span", "message-subject", message.subject || "sans objet"));
|
|
882
934
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
if (message.priority === "low") head.appendChild(tag("tag", "priorité basse"));
|
|
889
|
-
head.appendChild(node("span", "message-time", formatShort(message.created_at)));
|
|
935
|
+
head.addEventListener("click", () => {
|
|
936
|
+
if (open) state.openMessages.delete(message.message_id);
|
|
937
|
+
else state.openMessages.add(message.message_id);
|
|
938
|
+
fillMessage(item, message, isLast);
|
|
939
|
+
});
|
|
890
940
|
column.appendChild(head);
|
|
891
941
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
942
|
+
if (!open) {
|
|
943
|
+
row.appendChild(column);
|
|
944
|
+
item.replaceChildren(row);
|
|
945
|
+
return;
|
|
895
946
|
}
|
|
896
|
-
|
|
947
|
+
|
|
948
|
+
// Ce qui suit la ligne de titre s'aligne sous le texte, pas sous la pastille.
|
|
949
|
+
const content = node("div", "message-content");
|
|
950
|
+
column.appendChild(content);
|
|
951
|
+
|
|
952
|
+
content.appendChild(messageText(message));
|
|
897
953
|
|
|
898
954
|
const foot = node("div", "message-foot");
|
|
899
955
|
foot.appendChild(
|
|
@@ -901,18 +957,18 @@ function messageRow(message, isLast) {
|
|
|
901
957
|
? tag("tag tag--ok", `lu par ${party(message.to).name}`, "check")
|
|
902
958
|
: tag("tag tag--warn", `non lu par ${party(message.to).name}`, "clock")
|
|
903
959
|
);
|
|
904
|
-
|
|
960
|
+
// Le volet de détail, lui, s'ouvre à côté : c'est un autre état que le dépli du corps.
|
|
961
|
+
const sheetOpen = state.messageId === message.message_id;
|
|
905
962
|
foot.appendChild(
|
|
906
|
-
button("btn btn--link",
|
|
907
|
-
if (
|
|
963
|
+
button("btn btn--link", sheetOpen ? "détails ouverts" : "détails", () => {
|
|
964
|
+
if (sheetOpen) closeSheet();
|
|
908
965
|
else openMessage(message.message_id);
|
|
909
966
|
})
|
|
910
967
|
);
|
|
911
|
-
|
|
968
|
+
content.appendChild(foot);
|
|
912
969
|
|
|
913
970
|
row.appendChild(column);
|
|
914
|
-
item.
|
|
915
|
-
return item;
|
|
971
|
+
item.replaceChildren(row);
|
|
916
972
|
}
|
|
917
973
|
|
|
918
974
|
/**
|
|
@@ -1049,6 +1105,9 @@ function closeSheet() {
|
|
|
1049
1105
|
async function openThread(threadId) {
|
|
1050
1106
|
state.threadId = threadId;
|
|
1051
1107
|
state.messageId = null;
|
|
1108
|
+
// Un fil s'ouvre replié, même celui qu'on rouvre : ce qui était déplié l'était pour
|
|
1109
|
+
// l'autre conversation.
|
|
1110
|
+
state.openMessages.clear();
|
|
1052
1111
|
el.sheet.hidden = true;
|
|
1053
1112
|
el.console.classList.remove("has-sheet");
|
|
1054
1113
|
await refreshThread();
|
|
@@ -992,11 +992,39 @@ body {
|
|
|
992
992
|
padding-left: 8px;
|
|
993
993
|
}
|
|
994
994
|
|
|
995
|
-
/*
|
|
996
|
-
|
|
995
|
+
/*
|
|
996
|
+
Colonne repliée, deux signes distincts et souvent simultanés.
|
|
997
|
+
|
|
998
|
+
L'anneau dit ce qui est **sélectionné** : c'est un contour, il entoure, il désigne — la
|
|
999
|
+
teinte de fond qui sert à ça dans la colonne dépliée n'a plus de place ici. La pastille
|
|
1000
|
+
dit qu'il reste du **non-lu** : un point posé sur l'avatar, la même convention que la
|
|
1001
|
+
liste des fils. L'un n'implique pas l'autre, ils doivent donc se lire séparément.
|
|
1002
|
+
*/
|
|
1003
|
+
.is-collapsed .agent.is-selected {
|
|
1004
|
+
background: none;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
.is-collapsed .agent.is-selected .avatar {
|
|
997
1008
|
box-shadow: 0 0 0 2px var(--bg), 0 0 0 4px var(--accent);
|
|
998
1009
|
}
|
|
999
1010
|
|
|
1011
|
+
.is-collapsed .agent.has-unread .avatar {
|
|
1012
|
+
position: relative;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
.is-collapsed .agent.has-unread .avatar::after {
|
|
1016
|
+
content: "";
|
|
1017
|
+
position: absolute;
|
|
1018
|
+
top: -1px;
|
|
1019
|
+
right: -1px;
|
|
1020
|
+
width: 9px;
|
|
1021
|
+
height: 9px;
|
|
1022
|
+
border-radius: 50%;
|
|
1023
|
+
background: var(--accent);
|
|
1024
|
+
/* Le liseré détache la pastille de l'avatar, quelle que soit la couleur de celui-ci. */
|
|
1025
|
+
box-shadow: 0 0 0 2px var(--bg);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1000
1028
|
.is-collapsed .agents {
|
|
1001
1029
|
overflow-x: hidden;
|
|
1002
1030
|
}
|
|
@@ -1320,14 +1348,25 @@ body {
|
|
|
1320
1348
|
.messages {
|
|
1321
1349
|
list-style: none;
|
|
1322
1350
|
margin: 0;
|
|
1323
|
-
|
|
1351
|
+
/* Le retrait latéral du texte vient d'ici plus du cadre d'un message : 20 + 12. */
|
|
1352
|
+
padding: 12px 20px 40px;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/*
|
|
1356
|
+
Un trait entre deux messages, et non un simple blanc.
|
|
1357
|
+
|
|
1358
|
+
Repliés, les messages se suivent de près : sans démarcation, on devine où l'un s'arrête
|
|
1359
|
+
à la seule présence de la pastille suivante. Le trait court sur toute la largeur de la
|
|
1360
|
+
liste, et l'écart de part et d'autre vient du cadre des messages eux-mêmes.
|
|
1361
|
+
*/
|
|
1362
|
+
.messages > li + li {
|
|
1363
|
+
border-top-width: 1px;
|
|
1364
|
+
border-top-style: solid;
|
|
1365
|
+
border-top-color: var(--border);
|
|
1324
1366
|
}
|
|
1325
1367
|
|
|
1326
1368
|
.message {
|
|
1327
|
-
display: flex;
|
|
1328
|
-
gap: 12px;
|
|
1329
1369
|
padding: 12px;
|
|
1330
|
-
margin: 0 -12px;
|
|
1331
1370
|
border-radius: var(--radius);
|
|
1332
1371
|
}
|
|
1333
1372
|
|
|
@@ -1342,15 +1381,83 @@ body {
|
|
|
1342
1381
|
}
|
|
1343
1382
|
|
|
1344
1383
|
.message-body-col {
|
|
1345
|
-
flex: 1;
|
|
1346
1384
|
min-width: 0;
|
|
1347
1385
|
}
|
|
1348
1386
|
|
|
1387
|
+
/*
|
|
1388
|
+
Le bandeau qui ouvre et referme le message.
|
|
1389
|
+
|
|
1390
|
+
Il tient tout ce qu'un message replié montre : la pastille, qui écrit à qui, l'heure, et
|
|
1391
|
+
la ligne de résumé. Rien n'y ressemble à un bouton tant qu'on ne le survole pas — c'est
|
|
1392
|
+
le fond gris qui dit alors que la zone entière est une cible, mieux qu'un chevron.
|
|
1393
|
+
|
|
1394
|
+
Deux colonnes, deux lignes : la pastille tient sur les deux, le texte occupe la seconde
|
|
1395
|
+
colonne — qui écrit à qui sur la première ligne, l'objet du message sur la seconde.
|
|
1396
|
+
*/
|
|
1349
1397
|
.message-head {
|
|
1398
|
+
display: grid;
|
|
1399
|
+
grid-template-columns: auto minmax(0, 1fr);
|
|
1400
|
+
align-items: center;
|
|
1401
|
+
gap: 2px 12px;
|
|
1402
|
+
width: 100%;
|
|
1403
|
+
padding: 6px;
|
|
1404
|
+
margin: -6px;
|
|
1405
|
+
border: 0;
|
|
1406
|
+
border-radius: var(--radius);
|
|
1407
|
+
background: none;
|
|
1408
|
+
font: inherit;
|
|
1409
|
+
color: inherit;
|
|
1410
|
+
text-align: left;
|
|
1411
|
+
cursor: pointer;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
.message-head:hover {
|
|
1415
|
+
background: var(--neutral-soft);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
.message-head:focus-visible {
|
|
1419
|
+
outline: 2px solid var(--accent);
|
|
1420
|
+
outline-offset: -2px;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
.message-head > .avatar {
|
|
1424
|
+
grid-row: span 2;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
/* Qui écrit à qui, et quand : l'heure est poussée au bout de la ligne. */
|
|
1428
|
+
.message-identity {
|
|
1350
1429
|
display: flex;
|
|
1351
|
-
align-items:
|
|
1430
|
+
align-items: center;
|
|
1352
1431
|
gap: 8px;
|
|
1353
1432
|
flex-wrap: wrap;
|
|
1433
|
+
min-width: 0;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/*
|
|
1437
|
+
L'objet du message, sur sa ligne, replié comme déplié : c'est ce qui identifie le
|
|
1438
|
+
message, et le bandeau ne doit pas changer de forme en s'ouvrant. Ce qui dépasse est
|
|
1439
|
+
coupé — la ligne suivante appartient au message suivant.
|
|
1440
|
+
*/
|
|
1441
|
+
.message-subject {
|
|
1442
|
+
font-weight: 600;
|
|
1443
|
+
min-width: 0;
|
|
1444
|
+
white-space: nowrap;
|
|
1445
|
+
overflow: hidden;
|
|
1446
|
+
text-overflow: ellipsis;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/* Le corps s'aligne sous le texte du bandeau, jamais sous la pastille. */
|
|
1450
|
+
.message-content {
|
|
1451
|
+
padding-left: 40px;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/* Non lu : le même point que dans la liste des fils, et non un anneau ni une couleur. */
|
|
1455
|
+
.message-dot {
|
|
1456
|
+
width: 8px;
|
|
1457
|
+
height: 8px;
|
|
1458
|
+
border-radius: 50%;
|
|
1459
|
+
background: var(--accent);
|
|
1460
|
+
flex: none;
|
|
1354
1461
|
}
|
|
1355
1462
|
|
|
1356
1463
|
.message-from {
|
|
@@ -1369,15 +1476,8 @@ body {
|
|
|
1369
1476
|
white-space: nowrap;
|
|
1370
1477
|
}
|
|
1371
1478
|
|
|
1372
|
-
.message-subject {
|
|
1373
|
-
margin: 8px 0 0;
|
|
1374
|
-
font-size: 13px;
|
|
1375
|
-
font-weight: 600;
|
|
1376
|
-
color: var(--muted);
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
1479
|
.message-text {
|
|
1380
|
-
margin:
|
|
1480
|
+
margin: 8px 0 0;
|
|
1381
1481
|
white-space: pre-wrap;
|
|
1382
1482
|
overflow-wrap: anywhere;
|
|
1383
1483
|
}
|
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.24";
|
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.24";
|