@imenam/mcp-messenger 1.0.14 → 1.0.16
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 +22 -7
- package/dist/src/core/store.js +36 -1
- package/dist/src/server/ui/app.js +90 -13
- package/dist/src/server/ui/markdown.js +340 -0
- package/dist/src/server/ui/styles.css +181 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -347,17 +347,29 @@ correspondants et l'état de lecture :
|
|
|
347
347
|
**toutes ses sessions**, cliquer une boîte sur **cette seule session**. La colonne se
|
|
348
348
|
replie sur les seuls avatars d'agents quand la place manque.
|
|
349
349
|
|
|
350
|
-
**Le classement se fait
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
par
|
|
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 ;
|
|
355
360
|
- **la liste des fils**, du plus récemment actif au plus ancien, chacun résumé par son
|
|
356
361
|
nom, ses correspondants, son dernier message et sa priorité la plus haute. Trois puces
|
|
357
362
|
la filtrent : tous, non lus, priorité haute ;
|
|
358
363
|
- **le lecteur du fil** : la conversation entière dans l'ordre, un bloc par message, avec
|
|
359
364
|
qui écrit à qui — en clair, pas en adresses — et une pastille disant si le destinataire
|
|
360
|
-
l'a lu
|
|
365
|
+
l'a lu. **Le corps d'un message est rendu comme du markdown** : titres, listes,
|
|
366
|
+
citations, tableaux, blocs de code, emphase et liens. C'est la langue dans laquelle les
|
|
367
|
+
agents s'écrivent, la console la lit donc ainsi plutôt que d'afficher sa syntaxe. Le
|
|
368
|
+
rendu construit des nœuds un par un et n'insère jamais de HTML : rien de ce qu'un agent
|
|
369
|
+
écrit ne peut devenir une balise, et seuls les liens `http`, `https` et `mailto` sont
|
|
370
|
+
suivis. Une image n'est jamais chargée, elle est montrée par son intitulé et son lien.
|
|
371
|
+
Les aperçus, eux, sont débarrassés de leur syntaxe par le serveur : ils tiennent sur une
|
|
372
|
+
ligne, où un dièse ou une paire d'astérisques ne serait que du bruit ;
|
|
361
373
|
- **le volet de détail** d'un message, ouvert à la demande : identifiants du message et du
|
|
362
374
|
fil, les deux noms et l'adresse de chaque boîte, et le tableau de distribution (rôle,
|
|
363
375
|
dossier, état). Tout ce qui se recopie a son bouton de copie ;
|
|
@@ -503,6 +515,7 @@ src/
|
|
|
503
515
|
server/watchers.ts # présence des veilleurs, en mémoire : état de connexion
|
|
504
516
|
server/start.ts # démarrage, arrêt propre, purge de la corbeille
|
|
505
517
|
server/ui/ # console web (HTML/CSS/JS sans dépendance)
|
|
518
|
+
server/ui/markdown.js # rendu markdown du corps des messages, en nœuds DOM
|
|
506
519
|
cli/watch.ts # mode veilleur : une ligne sur stdout par message entrant
|
|
507
520
|
cli/watch-handoff.ts # relais 0600 entre la session MCP et son veilleur
|
|
508
521
|
client/ # client HTTP utilisé par le mode MCP
|
|
@@ -512,7 +525,9 @@ test/ # store, config, API HTTP, outils MCP, binaire réel
|
|
|
512
525
|
|
|
513
526
|
### Tests
|
|
514
527
|
|
|
515
|
-
|
|
528
|
+
187 tests couvrant : le rendu markdown des messages et l'aplatissement des aperçus (dont
|
|
529
|
+
l'innocuité du rendu : ni HTML interprété, ni lien vers un autre schéma qu'`http`, `https`
|
|
530
|
+
ou `mailto`), le stockage et l'isolation des boîtes, la validation de configuration,
|
|
516
531
|
l'API HTTP et son authentification (refus systématique sans jeton, jeton partagé, jetons par
|
|
517
532
|
agent, jeton de console distinct), les outils MCP de bout en bout (deux agents qui
|
|
518
533
|
conversent à travers un vrai serveur), l'allocation des boîtes par session — deux processus
|
package/dist/src/core/store.js
CHANGED
|
@@ -848,9 +848,44 @@ function clamp(value, min, max) {
|
|
|
848
848
|
return Math.min(max, Math.max(min, Math.trunc(value)));
|
|
849
849
|
}
|
|
850
850
|
function preview(body) {
|
|
851
|
-
const flat = body.replace(/\s+/g, " ").trim();
|
|
851
|
+
const flat = stripMarkdown(body).replace(/\s+/g, " ").trim();
|
|
852
852
|
return flat.length <= PREVIEW_LENGTH ? flat : `${flat.slice(0, PREVIEW_LENGTH - 1)}…`;
|
|
853
853
|
}
|
|
854
|
+
/**
|
|
855
|
+
* Le corps d'un message est du markdown ; un aperçu, non — il tient sur une ligne, dans
|
|
856
|
+
* une liste, sans mise en forme pour le porter. La syntaxe y devient donc du bruit : on
|
|
857
|
+
* garde ce que le markdown dit, on jette ce qui sert à le mettre en forme.
|
|
858
|
+
*
|
|
859
|
+
* C'est un aplatissement, pas une analyse : l'aperçu n'a pas à rendre compte de la
|
|
860
|
+
* structure du message, seulement à en donner les premiers mots lisibles.
|
|
861
|
+
*/
|
|
862
|
+
function stripMarkdown(body) {
|
|
863
|
+
return (body
|
|
864
|
+
// Un bloc de code n'a rien de lisible en 160 caractères : seule sa langue renseigne.
|
|
865
|
+
.replace(/^ {0,3}(```|~~~)[^\n]*\n[\s\S]*?(?:\n {0,3}\1[^\n]*|$)/gm, (block) => {
|
|
866
|
+
const language = /^ {0,3}(?:```|~~~)\s*([^\s`]+)/.exec(block)?.[1];
|
|
867
|
+
return language ? `[code ${language}]` : "[code]";
|
|
868
|
+
})
|
|
869
|
+
.replace(/`+([^`]+)`+/g, "$1")
|
|
870
|
+
// Images et liens : l'intitulé porte le sens, l'URL encombre.
|
|
871
|
+
.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
872
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
|
|
873
|
+
.replace(/<((?:https?:\/\/|mailto:)[^\s>]+)>/g, "$1")
|
|
874
|
+
// Marqueurs de bloc en tête de ligne : titres, citations, puces, filets.
|
|
875
|
+
.replace(/^ {0,3}#{1,6}\s+/gm, "")
|
|
876
|
+
.replace(/^ {0,3}>[ \t]?/gm, "")
|
|
877
|
+
.replace(/^\s*([-*+]|\d{1,9}[.)])[ \t]+/gm, "· ")
|
|
878
|
+
.replace(/^ {0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/gm, " ")
|
|
879
|
+
// Un tableau perd sa grille : ses cellules restent, séparées.
|
|
880
|
+
.replace(/^ {0,3}\|?[ \t]*:?-+:?[ \t]*(\|[ \t]*:?-+:?[ \t]*)+\|?[ \t]*$/gm, " ")
|
|
881
|
+
.replace(/^[ \t]*\|/gm, "")
|
|
882
|
+
.replace(/\|[ \t]*$/gm, "")
|
|
883
|
+
.replace(/[ \t]*\|[ \t]*/g, " · ")
|
|
884
|
+
// Emphase : les marques tombent, le texte reste.
|
|
885
|
+
.replace(/(\*\*|__)(\S[\s\S]*?\S)\1/g, "$2")
|
|
886
|
+
.replace(/~~(\S[\s\S]*?\S)~~/g, "$1")
|
|
887
|
+
.replace(/(?<![\w*])([*_])(\S[^*_\n]*?\S)\1(?![\w*])/g, "$2"));
|
|
888
|
+
}
|
|
854
889
|
/** Échappe les jokers SQL : la recherche reste littérale, `%` cherche bien un `%`. */
|
|
855
890
|
function escapeLike(value) {
|
|
856
891
|
return value.replace(/[\\%_]/g, (char) => `\\${char}`);
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Tout le contenu affiché provient d'agents : il est inséré via textContent uniquement,
|
|
4
4
|
// jamais par innerHTML. Les seuls fragments de balisage construits ici sont les icônes,
|
|
5
|
-
// écrites en dur dans ce fichier.
|
|
5
|
+
// écrites en dur dans ce fichier. Le corps des messages passe par `renderMarkdown`, qui
|
|
6
|
+
// bâtit lui aussi des nœuds un par un et n'ouvre donc pas non plus de brèche.
|
|
6
7
|
//
|
|
7
8
|
// L'authentification tient dans un cookie de session HttpOnly posé par `/ui/login` :
|
|
8
9
|
// ce script ne voit jamais le jeton et n'écrit rien dans le navigateur.
|
|
9
10
|
|
|
11
|
+
import { renderMarkdown } from "./markdown.js";
|
|
12
|
+
|
|
10
13
|
const REFRESH_MS = 5000;
|
|
11
14
|
|
|
12
15
|
// Une adresse ne dit rien à qui lit la console : elle sert de graine à la couleur de
|
|
@@ -33,6 +36,8 @@ const state = {
|
|
|
33
36
|
selection: { kind: "all", key: null },
|
|
34
37
|
/** Noms d'agents dépliés dans la colonne de gauche. Tout est replié au départ. */
|
|
35
38
|
expanded: new Set(),
|
|
39
|
+
/** Agents dont on a demandé à voir les boîtes vides. Elles sont cachées au départ. */
|
|
40
|
+
showEmpty: new Set(),
|
|
36
41
|
/** "all" | "unread" | "high" — les puces au-dessus de la liste des fils. */
|
|
37
42
|
filter: "all",
|
|
38
43
|
search: "",
|
|
@@ -287,23 +292,41 @@ function groupByAgent(boxes) {
|
|
|
287
292
|
for (const box of boxes) {
|
|
288
293
|
const key = box.agent_name || box.id;
|
|
289
294
|
if (!groups.has(key)) {
|
|
290
|
-
groups.set(key, { key, name: box.agent_name || boxLabel(box), boxes: [], unread: 0, activity: "" });
|
|
295
|
+
groups.set(key, { key, name: box.agent_name || boxLabel(box), boxes: [], unread: 0, filled: 0, activity: "" });
|
|
291
296
|
}
|
|
292
297
|
const group = groups.get(key);
|
|
293
298
|
group.boxes.push(box);
|
|
294
299
|
group.unread += box.unread;
|
|
300
|
+
if (!isEmptyBox(box)) group.filled += 1;
|
|
295
301
|
// L'agent hérite de l'activité de sa session la plus vivante.
|
|
296
302
|
if (activityOf(box) > group.activity) group.activity = activityOf(box);
|
|
297
303
|
}
|
|
298
304
|
const list = [...groups.values()];
|
|
299
|
-
for (const group of list) group.boxes.sort(
|
|
305
|
+
for (const group of list) group.boxes.sort(byBox);
|
|
300
306
|
// Le classement est refait ici plutôt que laissé à l'ordre d'arrivée de l'annuaire :
|
|
301
|
-
// ce que la colonne promet
|
|
302
|
-
//
|
|
303
|
-
list.sort(
|
|
307
|
+
// ce que la colonne promet ne doit pas dépendre du hasard d'un tri fait ailleurs. Un
|
|
308
|
+
// agent dont aucune session n'a rien échangé passe derrière les autres, comme ses boîtes.
|
|
309
|
+
list.sort(
|
|
310
|
+
(a, b) =>
|
|
311
|
+
compareEmpty(a.filled === 0, b.filled === 0) ||
|
|
312
|
+
compareActivity(a.activity, b.activity) ||
|
|
313
|
+
(a.key < b.key ? -1 : a.key > b.key ? 1 : 0)
|
|
314
|
+
);
|
|
304
315
|
return list;
|
|
305
316
|
}
|
|
306
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Une boîte est vide tant que rien n'y est passé, ni envoi ni réception.
|
|
320
|
+
*
|
|
321
|
+
* Le serveur alloue une boîte à chaque session ouverte : beaucoup n'échangeront jamais
|
|
322
|
+
* rien et n'ont donc rien à dire à qui supervise. Une boîte vide n'a par construction
|
|
323
|
+
* aucun non-lu — `received` à zéro l'implique — la masquer ne cache donc jamais de
|
|
324
|
+
* courrier en attente.
|
|
325
|
+
*/
|
|
326
|
+
function isEmptyBox(box) {
|
|
327
|
+
return box.sent + box.received === 0;
|
|
328
|
+
}
|
|
329
|
+
|
|
307
330
|
/** Dernière activité d'une boîte, ou chaîne vide quand elle n'a jamais rien fait. */
|
|
308
331
|
function activityOf(box) {
|
|
309
332
|
return box.last_activity_at || "";
|
|
@@ -320,9 +343,23 @@ function compareActivity(left, right) {
|
|
|
320
343
|
return left < right ? 1 : -1;
|
|
321
344
|
}
|
|
322
345
|
|
|
323
|
-
/**
|
|
324
|
-
function
|
|
325
|
-
|
|
346
|
+
/** Le plein avant le vide. Premier critère du classement, avant même la date. */
|
|
347
|
+
function compareEmpty(left, right) {
|
|
348
|
+
if (left === right) return 0;
|
|
349
|
+
return left ? 1 : -1;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Deux classements emboîtés : d'abord les boîtes qui ont servi, puis les vides ; dans
|
|
354
|
+
* chacun des deux, la plus récemment active en tête. À égalité, l'adresse départage, pour
|
|
355
|
+
* que l'ordre ne bouge pas d'un rafraîchissement à l'autre.
|
|
356
|
+
*/
|
|
357
|
+
function byBox(a, b) {
|
|
358
|
+
return (
|
|
359
|
+
compareEmpty(isEmptyBox(a), isEmptyBox(b)) ||
|
|
360
|
+
compareActivity(activityOf(a), activityOf(b)) ||
|
|
361
|
+
(a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
|
|
362
|
+
);
|
|
326
363
|
}
|
|
327
364
|
|
|
328
365
|
function isSelected(kind, key) {
|
|
@@ -415,12 +452,17 @@ function groupRow(group) {
|
|
|
415
452
|
)
|
|
416
453
|
);
|
|
417
454
|
|
|
455
|
+
const empty = group.boxes.length - group.filled;
|
|
456
|
+
// Le décompte annonce ce que le dépli va montrer : sans quoi « 5 boîtes » suivi de deux
|
|
457
|
+
// lignes se lirait comme un bug plutôt que comme des boîtes vides mises de côté.
|
|
458
|
+
const counts = empty > 0 ? `${plural(group.filled, "boîte")} · ${plural(empty, "vide")}` : plural(group.filled, "boîte");
|
|
459
|
+
|
|
418
460
|
const text = node("div", "agent-text");
|
|
419
461
|
text.appendChild(node("div", "agent-name", group.name));
|
|
420
|
-
text.appendChild(node("div", "agent-box",
|
|
462
|
+
text.appendChild(node("div", "agent-box", counts));
|
|
421
463
|
row.appendChild(text);
|
|
422
464
|
if (group.unread > 0) row.appendChild(node("span", "count count--accent", String(group.unread)));
|
|
423
|
-
row.title = `${group.name} · ${
|
|
465
|
+
row.title = `${group.name} · ${counts}`;
|
|
424
466
|
row.addEventListener("click", () => {
|
|
425
467
|
state.expanded.add(group.key);
|
|
426
468
|
select("agent", group.key);
|
|
@@ -430,15 +472,37 @@ function groupRow(group) {
|
|
|
430
472
|
|
|
431
473
|
if (open) {
|
|
432
474
|
const list = node("ul", "box-list");
|
|
433
|
-
|
|
475
|
+
// Les vides sont derrière dans `group.boxes` : les montrer, c'est allonger la liste,
|
|
476
|
+
// jamais en réordonner le début.
|
|
477
|
+
const revealed = state.showEmpty.has(group.key);
|
|
478
|
+
for (const box of group.boxes) {
|
|
479
|
+
if (isEmptyBox(box) && !revealed) continue;
|
|
480
|
+
list.appendChild(boxRow(box));
|
|
481
|
+
}
|
|
482
|
+
if (empty > 0) list.appendChild(emptyToggleRow(group, empty, revealed));
|
|
434
483
|
item.appendChild(list);
|
|
435
484
|
}
|
|
436
485
|
return item;
|
|
437
486
|
}
|
|
438
487
|
|
|
488
|
+
/** Dernière ligne du dépli : ouvre ou referme les boîtes qui n'ont jamais rien échangé. */
|
|
489
|
+
function emptyToggleRow(group, empty, revealed) {
|
|
490
|
+
const item = node("li");
|
|
491
|
+
const what = empty > 1 ? `les ${empty} boîtes vides` : "la boîte vide";
|
|
492
|
+
const toggle = button("box-empty-toggle", `${revealed ? "Masquer" : "Afficher"} ${what}`, () => {
|
|
493
|
+
if (revealed) state.showEmpty.delete(group.key);
|
|
494
|
+
else state.showEmpty.add(group.key);
|
|
495
|
+
renderAgents({ agents: [...state.directory.values()], threads: currentThreadCount() });
|
|
496
|
+
});
|
|
497
|
+
toggle.setAttribute("aria-expanded", revealed ? "true" : "false");
|
|
498
|
+
item.appendChild(toggle);
|
|
499
|
+
return item;
|
|
500
|
+
}
|
|
501
|
+
|
|
439
502
|
function boxRow(box) {
|
|
440
503
|
const item = node("li");
|
|
441
504
|
const classes = ["agent", "box"];
|
|
505
|
+
if (isEmptyBox(box)) classes.push("is-empty");
|
|
442
506
|
if (isSelected("box", box.id)) classes.push("is-selected");
|
|
443
507
|
if (box.unread > 0) classes.push("has-unread");
|
|
444
508
|
if (!box.registered) classes.push("agent--muted");
|
|
@@ -626,7 +690,7 @@ function messageRow(message, isLast) {
|
|
|
626
690
|
if (message.subject && message.subject !== message.thread_title) {
|
|
627
691
|
column.appendChild(node("p", "message-subject", message.subject));
|
|
628
692
|
}
|
|
629
|
-
column.appendChild(
|
|
693
|
+
column.appendChild(messageText(message));
|
|
630
694
|
|
|
631
695
|
const foot = node("div", "message-foot");
|
|
632
696
|
foot.appendChild(
|
|
@@ -648,6 +712,18 @@ function messageRow(message, isLast) {
|
|
|
648
712
|
return item;
|
|
649
713
|
}
|
|
650
714
|
|
|
715
|
+
/**
|
|
716
|
+
* Le corps d'un message, rendu comme du markdown : c'est la langue dans laquelle les
|
|
717
|
+
* agents s'écrivent. L'aperçu, lui, arrive déjà débarrassé de sa syntaxe par le serveur
|
|
718
|
+
* et reste donc du texte simple.
|
|
719
|
+
*/
|
|
720
|
+
function messageText(message) {
|
|
721
|
+
if (!message.body) return node("p", "message-text", message.preview || "");
|
|
722
|
+
const container = node("div", "message-text markdown");
|
|
723
|
+
container.appendChild(renderMarkdown(message.body));
|
|
724
|
+
return container;
|
|
725
|
+
}
|
|
726
|
+
|
|
651
727
|
// --- Volet de détail -------------------------------------------------------
|
|
652
728
|
|
|
653
729
|
function renderSheet(message) {
|
|
@@ -894,6 +970,7 @@ el.logout.addEventListener("click", async () => {
|
|
|
894
970
|
await fetch("/ui/logout", { method: "POST", credentials: "same-origin" }).catch(() => null);
|
|
895
971
|
state.selection = { kind: "all", key: null };
|
|
896
972
|
state.expanded = new Set();
|
|
973
|
+
state.showEmpty = new Set();
|
|
897
974
|
state.threadId = null;
|
|
898
975
|
state.messageId = null;
|
|
899
976
|
state.directory = new Map();
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendu Markdown de la console.
|
|
3
|
+
*
|
|
4
|
+
* Les agents s'écrivent en markdown : un corps de message affiché tel quel se lit mal
|
|
5
|
+
* (titres en dièses, listes en tirets, blocs de code noyés dans le texte). Ce module
|
|
6
|
+
* traduit ce markdown en DOM.
|
|
7
|
+
*
|
|
8
|
+
* Il construit des nœuds, jamais du HTML : tout texte venu d'un message passe par
|
|
9
|
+
* `textContent`, donc rien de ce qu'un agent écrit ne peut devenir une balise. Les seules
|
|
10
|
+
* URL suivies sont http, https et mailto ; le reste retombe en texte.
|
|
11
|
+
*
|
|
12
|
+
* La couverture est celle du markdown qu'écrivent les agents : titres, listes, citations,
|
|
13
|
+
* code, tableaux, filets, emphase, liens. Pas de HTML brut, pas de notes de bas de page.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})\s*([^`\s]*)\s*$/;
|
|
17
|
+
const HEADING = /^ {0,3}(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
18
|
+
const RULE = /^ {0,3}([-*_])[ \t]*(?:\1[ \t]*){2,}$/;
|
|
19
|
+
const QUOTE = /^ {0,3}>[ \t]?(.*)$/;
|
|
20
|
+
const BULLET = /^(\s*)([-*+])[ \t]+(.*)$/;
|
|
21
|
+
const ORDERED = /^(\s*)(\d{1,9})[.)][ \t]+(.*)$/;
|
|
22
|
+
const TABLE_RULE = /^ {0,3}\|?[ \t]*:?-+:?[ \t]*(\|[ \t]*:?-+:?[ \t]*)+\|?[ \t]*$/;
|
|
23
|
+
const SAFE_HREF = /^(https?:\/\/|mailto:)/i;
|
|
24
|
+
|
|
25
|
+
/** Rend un corps de message et renvoie un fragment prêt à être inséré. */
|
|
26
|
+
export function renderMarkdown(source) {
|
|
27
|
+
const fragment = document.createDocumentFragment();
|
|
28
|
+
const lines = String(source ?? "")
|
|
29
|
+
.replace(/\r\n?/g, "\n")
|
|
30
|
+
.split("\n");
|
|
31
|
+
for (const child of parseBlocks(lines)) fragment.appendChild(child);
|
|
32
|
+
return fragment;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --- Blocs -----------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
function parseBlocks(lines) {
|
|
38
|
+
const out = [];
|
|
39
|
+
let i = 0;
|
|
40
|
+
|
|
41
|
+
while (i < lines.length) {
|
|
42
|
+
const line = lines[i];
|
|
43
|
+
|
|
44
|
+
if (!line.trim()) {
|
|
45
|
+
i += 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const fence = FENCE.exec(line);
|
|
50
|
+
if (fence) {
|
|
51
|
+
const closing = new RegExp(`^ {0,3}\\${fence[1][0]}{${fence[1].length},}\\s*$`);
|
|
52
|
+
const body = [];
|
|
53
|
+
i += 1;
|
|
54
|
+
while (i < lines.length && !closing.test(lines[i])) {
|
|
55
|
+
body.push(lines[i]);
|
|
56
|
+
i += 1;
|
|
57
|
+
}
|
|
58
|
+
i += 1; // la clôture, ou la fin du texte si le bloc n'a jamais été refermé
|
|
59
|
+
out.push(codeBlock(body.join("\n"), fence[2]));
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const heading = HEADING.exec(line);
|
|
64
|
+
if (heading) {
|
|
65
|
+
const el = document.createElement(`h${heading[1].length}`);
|
|
66
|
+
el.className = "md-heading";
|
|
67
|
+
renderInline(heading[2], el);
|
|
68
|
+
out.push(el);
|
|
69
|
+
i += 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (RULE.test(line)) {
|
|
74
|
+
const hr = document.createElement("hr");
|
|
75
|
+
hr.className = "md-rule";
|
|
76
|
+
out.push(hr);
|
|
77
|
+
i += 1;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (QUOTE.test(line)) {
|
|
82
|
+
const inner = [];
|
|
83
|
+
while (i < lines.length && QUOTE.test(lines[i])) {
|
|
84
|
+
inner.push(QUOTE.exec(lines[i])[1]);
|
|
85
|
+
i += 1;
|
|
86
|
+
}
|
|
87
|
+
const quote = document.createElement("blockquote");
|
|
88
|
+
quote.className = "md-quote";
|
|
89
|
+
for (const child of parseBlocks(inner)) quote.appendChild(child);
|
|
90
|
+
out.push(quote);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (BULLET.test(line) || ORDERED.test(line)) {
|
|
95
|
+
const [list, next] = parseList(lines, i);
|
|
96
|
+
out.push(list);
|
|
97
|
+
i = next;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (opensTable(lines, i)) {
|
|
102
|
+
const [table, next] = parseTable(lines, i);
|
|
103
|
+
if (table) {
|
|
104
|
+
out.push(table);
|
|
105
|
+
i = next;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Paragraphe : tout jusqu'à la ligne vide ou au premier bloc d'un autre genre.
|
|
111
|
+
const text = [];
|
|
112
|
+
while (i < lines.length && lines[i].trim() && !(text.length > 0 && startsBlock(lines, i))) {
|
|
113
|
+
text.push(lines[i].trim());
|
|
114
|
+
i += 1;
|
|
115
|
+
}
|
|
116
|
+
const paragraph = document.createElement("p");
|
|
117
|
+
paragraph.className = "md-p";
|
|
118
|
+
renderInline(text.join("\n"), paragraph);
|
|
119
|
+
out.push(paragraph);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Une ligne qui, au milieu d'un paragraphe, ouvre malgré tout un autre bloc. */
|
|
126
|
+
function startsBlock(lines, i) {
|
|
127
|
+
const line = lines[i];
|
|
128
|
+
return (
|
|
129
|
+
FENCE.test(line) ||
|
|
130
|
+
HEADING.test(line) ||
|
|
131
|
+
RULE.test(line) ||
|
|
132
|
+
QUOTE.test(line) ||
|
|
133
|
+
BULLET.test(line) ||
|
|
134
|
+
ORDERED.test(line) ||
|
|
135
|
+
opensTable(lines, i)
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function opensTable(lines, i) {
|
|
140
|
+
return i + 1 < lines.length && lines[i].includes("|") && TABLE_RULE.test(lines[i + 1]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function codeBlock(code, language) {
|
|
144
|
+
const pre = document.createElement("pre");
|
|
145
|
+
pre.className = "md-code";
|
|
146
|
+
const el = document.createElement("code");
|
|
147
|
+
if (language) {
|
|
148
|
+
el.className = `language-${language}`;
|
|
149
|
+
pre.dataset.language = language;
|
|
150
|
+
}
|
|
151
|
+
el.textContent = code;
|
|
152
|
+
pre.appendChild(el);
|
|
153
|
+
return pre;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Une liste et tout ce qu'elle contient. L'indentation seule décide de l'imbrication :
|
|
158
|
+
* une ligne plus rentrée que sa puce appartient à l'article ouvert, et c'est en le
|
|
159
|
+
* relisant comme un texte à part entière que la sous-liste apparaît.
|
|
160
|
+
*/
|
|
161
|
+
function parseList(lines, start) {
|
|
162
|
+
const ordered = !BULLET.test(lines[start]);
|
|
163
|
+
const first = ordered ? ORDERED.exec(lines[start]) : BULLET.exec(lines[start]);
|
|
164
|
+
const indent = first[1].length;
|
|
165
|
+
|
|
166
|
+
const list = document.createElement(ordered ? "ol" : "ul");
|
|
167
|
+
list.className = "md-list";
|
|
168
|
+
if (ordered && Number(first[2]) !== 1) list.start = Number(first[2]);
|
|
169
|
+
|
|
170
|
+
let i = start;
|
|
171
|
+
while (i < lines.length) {
|
|
172
|
+
if (!lines[i].trim()) {
|
|
173
|
+
// Une ligne vide ne ferme la liste que si plus rien derrière ne lui appartient.
|
|
174
|
+
if (!continues(lines, i, indent)) break;
|
|
175
|
+
i += 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const match = ordered ? ORDERED.exec(lines[i]) : BULLET.exec(lines[i]);
|
|
180
|
+
if (!match || match[1].length !== indent) break;
|
|
181
|
+
|
|
182
|
+
const own = [match[3]];
|
|
183
|
+
i += 1;
|
|
184
|
+
while (i < lines.length && (lines[i].trim() ? leadingSpaces(lines[i]) > indent : continues(lines, i, indent + 1))) {
|
|
185
|
+
own.push(lines[i].slice(Math.min(leadingSpaces(lines[i]), indent + 2)));
|
|
186
|
+
i += 1;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const item = document.createElement("li");
|
|
190
|
+
item.className = "md-item";
|
|
191
|
+
const children = parseBlocks(own);
|
|
192
|
+
// Un article d'une seule ligne n'a pas besoin d'un paragraphe autour de son texte.
|
|
193
|
+
if (children.length === 1 && children[0].tagName === "P") item.append(...children[0].childNodes);
|
|
194
|
+
else for (const child of children) item.appendChild(child);
|
|
195
|
+
list.appendChild(item);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return [list, i];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Vrai si, après la ligne vide `i`, du texte reste rentré d'au moins `indent`. */
|
|
202
|
+
function continues(lines, i, indent) {
|
|
203
|
+
for (let j = i + 1; j < lines.length; j += 1) {
|
|
204
|
+
if (!lines[j].trim()) continue;
|
|
205
|
+
return leadingSpaces(lines[j]) >= indent;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function leadingSpaces(line) {
|
|
211
|
+
return line.length - line.replace(/^[ \t]*/, "").length;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseTable(lines, start) {
|
|
215
|
+
const header = splitRow(lines[start]);
|
|
216
|
+
const alignments = splitRow(lines[start + 1]).map((cell) => {
|
|
217
|
+
const left = cell.startsWith(":");
|
|
218
|
+
const right = cell.endsWith(":");
|
|
219
|
+
if (left && right) return "center";
|
|
220
|
+
if (right) return "right";
|
|
221
|
+
return left ? "left" : null;
|
|
222
|
+
});
|
|
223
|
+
if (header.length !== alignments.length) return [null, start];
|
|
224
|
+
|
|
225
|
+
const table = document.createElement("table");
|
|
226
|
+
table.className = "md-table";
|
|
227
|
+
const thead = document.createElement("thead");
|
|
228
|
+
thead.appendChild(row(header, alignments, "th"));
|
|
229
|
+
table.appendChild(thead);
|
|
230
|
+
|
|
231
|
+
const tbody = document.createElement("tbody");
|
|
232
|
+
let i = start + 2;
|
|
233
|
+
while (i < lines.length && lines[i].trim() && lines[i].includes("|")) {
|
|
234
|
+
tbody.appendChild(row(splitRow(lines[i]), alignments, "td"));
|
|
235
|
+
i += 1;
|
|
236
|
+
}
|
|
237
|
+
table.appendChild(tbody);
|
|
238
|
+
return [table, i];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function splitRow(line) {
|
|
242
|
+
return line
|
|
243
|
+
.trim()
|
|
244
|
+
.replace(/^\|/, "")
|
|
245
|
+
.replace(/\|$/, "")
|
|
246
|
+
.split("|")
|
|
247
|
+
.map((cell) => cell.trim());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function row(cells, alignments, tag) {
|
|
251
|
+
const tr = document.createElement("tr");
|
|
252
|
+
alignments.forEach((align, index) => {
|
|
253
|
+
const cell = document.createElement(tag);
|
|
254
|
+
if (align) cell.style.textAlign = align;
|
|
255
|
+
renderInline(cells[index] ?? "", cell);
|
|
256
|
+
tr.appendChild(cell);
|
|
257
|
+
});
|
|
258
|
+
return tr;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// --- Enrichissements de ligne ----------------------------------------------
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Les motifs sont confrontés au texte à chaque tour, et celui qui accroche le plus tôt
|
|
265
|
+
* l'emporte. Le code passe en premier à égalité de position : son contenu ne doit plus
|
|
266
|
+
* être relu, un `**` dans un extrait de code est du code et non de la graisse.
|
|
267
|
+
*/
|
|
268
|
+
const INLINE = [
|
|
269
|
+
{ re: /(`+)([\s\S]+?)\1/, build: (m) => textNode("code", "md-inline-code", m[2].trim()) },
|
|
270
|
+
// La cible tolère une paire de parenthèses : une URL en contient parfois, et le lien
|
|
271
|
+
// ne doit pas se refermer sur la première rencontrée.
|
|
272
|
+
{ re: /!\[([^\]]*)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+"[^"]*")?\)/, build: (m) => image(m[1], m[2]) },
|
|
273
|
+
{ re: /\[([^\]]*)\]\(((?:[^\s()]|\([^\s()]*\))+)(?:\s+"[^"]*")?\)/, build: (m) => link(m[2], m[1]) },
|
|
274
|
+
{ re: /<((?:https?:\/\/|mailto:)[^\s>]+)>/, build: (m) => link(m[1], m[1]) },
|
|
275
|
+
{ re: /(\*\*|__)(?=\S)([\s\S]+?\S)\1/, build: (m) => inlineNode("strong", "md-strong", m[2]) },
|
|
276
|
+
{ re: /(?<![\w*])(\*|_)(?=\S)([^*_\n]+?\S)\1(?![\w*])/, build: (m) => inlineNode("em", "md-em", m[2]) },
|
|
277
|
+
{ re: /~~(?=\S)([\s\S]+?\S)~~/, build: (m) => inlineNode("del", "md-del", m[1]) },
|
|
278
|
+
{ re: /(?<![("[])https?:\/\/[^\s<>"'`]*[^\s<>"'`.,;:!?)\]]/, build: (m) => link(m[0], m[0]) },
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
function renderInline(text, parent) {
|
|
282
|
+
let rest = text;
|
|
283
|
+
|
|
284
|
+
while (rest) {
|
|
285
|
+
let best = null;
|
|
286
|
+
for (const pattern of INLINE) {
|
|
287
|
+
const match = pattern.re.exec(rest);
|
|
288
|
+
if (match && (!best || match.index < best.match.index)) best = { match, pattern };
|
|
289
|
+
if (best && best.match.index === 0) break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!best) break;
|
|
293
|
+
appendText(parent, rest.slice(0, best.match.index));
|
|
294
|
+
parent.appendChild(best.pattern.build(best.match));
|
|
295
|
+
rest = rest.slice(best.match.index + best.match[0].length);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
appendText(parent, rest);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Les retours à la ligne d'un paragraphe sont voulus : un agent aligne, il n'habille pas. */
|
|
302
|
+
function appendText(parent, text) {
|
|
303
|
+
if (!text) return;
|
|
304
|
+
text.split("\n").forEach((part, index) => {
|
|
305
|
+
if (index > 0) parent.appendChild(document.createElement("br"));
|
|
306
|
+
if (part) parent.appendChild(document.createTextNode(part));
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function textNode(tag, className, text) {
|
|
311
|
+
const el = document.createElement(tag);
|
|
312
|
+
el.className = className;
|
|
313
|
+
el.textContent = text;
|
|
314
|
+
return el;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function inlineNode(tag, className, text) {
|
|
318
|
+
const el = document.createElement(tag);
|
|
319
|
+
el.className = className;
|
|
320
|
+
renderInline(text, el);
|
|
321
|
+
return el;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function link(href, label) {
|
|
325
|
+
if (!SAFE_HREF.test(href)) return document.createTextNode(label || href);
|
|
326
|
+
const el = document.createElement("a");
|
|
327
|
+
el.className = "md-link";
|
|
328
|
+
el.href = href;
|
|
329
|
+
el.target = "_blank";
|
|
330
|
+
el.rel = "noopener noreferrer";
|
|
331
|
+
appendText(el, label || href);
|
|
332
|
+
return el;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Une image distante est montrée par son intitulé et son lien : la console ne va rien chercher dehors. */
|
|
336
|
+
function image(alt, src) {
|
|
337
|
+
const el = link(src, alt || src);
|
|
338
|
+
if (el.nodeType === 1) el.classList.add("md-image-link");
|
|
339
|
+
return el;
|
|
340
|
+
}
|
|
@@ -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;
|
|
@@ -1003,6 +1039,151 @@ body {
|
|
|
1003
1039
|
overflow-wrap: anywhere;
|
|
1004
1040
|
}
|
|
1005
1041
|
|
|
1042
|
+
/* Le markdown est déjà mis en forme : plus besoin de conserver les blancs du source. */
|
|
1043
|
+
.message-text.markdown {
|
|
1044
|
+
white-space: normal;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/* --- Corps de message rendu en markdown --- */
|
|
1048
|
+
|
|
1049
|
+
.markdown > *:first-child {
|
|
1050
|
+
margin-top: 0;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.markdown > *:last-child {
|
|
1054
|
+
margin-bottom: 0;
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
.md-p {
|
|
1058
|
+
margin: 0 0 8px;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
.markdown .md-heading {
|
|
1062
|
+
margin: 14px 0 6px;
|
|
1063
|
+
line-height: 1.35;
|
|
1064
|
+
font-weight: 600;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
.markdown h1.md-heading {
|
|
1068
|
+
font-size: 17px;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
.markdown h2.md-heading {
|
|
1072
|
+
font-size: 15px;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
.markdown h3.md-heading,
|
|
1076
|
+
.markdown h4.md-heading,
|
|
1077
|
+
.markdown h5.md-heading,
|
|
1078
|
+
.markdown h6.md-heading {
|
|
1079
|
+
font-size: 14px;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
.markdown h4.md-heading,
|
|
1083
|
+
.markdown h5.md-heading,
|
|
1084
|
+
.markdown h6.md-heading {
|
|
1085
|
+
color: var(--muted);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
.md-list {
|
|
1089
|
+
margin: 0 0 8px;
|
|
1090
|
+
padding-left: 20px;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
.md-item {
|
|
1094
|
+
margin: 2px 0;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/* Un article qui porte une sous-liste passe par un paragraphe : il ne doit pas s'en écarter. */
|
|
1098
|
+
.md-item > .md-p {
|
|
1099
|
+
margin-bottom: 2px;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
.md-list .md-list {
|
|
1103
|
+
margin: 2px 0;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
.md-quote {
|
|
1107
|
+
margin: 0 0 8px;
|
|
1108
|
+
padding: 2px 0 2px 10px;
|
|
1109
|
+
border-left: 3px solid var(--border);
|
|
1110
|
+
color: var(--muted);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
.md-rule {
|
|
1114
|
+
margin: 12px 0;
|
|
1115
|
+
border: 0;
|
|
1116
|
+
border-top: 1px solid var(--border);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/* La langue annoncée par l'agent, posée en coin du bloc : elle dit ce qu'on lit. */
|
|
1120
|
+
.md-code[data-language]::before {
|
|
1121
|
+
content: attr(data-language);
|
|
1122
|
+
display: block;
|
|
1123
|
+
margin-bottom: 6px;
|
|
1124
|
+
color: var(--muted);
|
|
1125
|
+
font-size: 11px;
|
|
1126
|
+
letter-spacing: 0.04em;
|
|
1127
|
+
text-transform: uppercase;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
.md-code {
|
|
1131
|
+
margin: 0 0 8px;
|
|
1132
|
+
padding: 10px 12px;
|
|
1133
|
+
border: 1px solid var(--border-soft);
|
|
1134
|
+
border-radius: var(--radius);
|
|
1135
|
+
background: var(--neutral-soft);
|
|
1136
|
+
overflow-x: auto;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
.md-code code {
|
|
1140
|
+
font: 12px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1141
|
+
white-space: pre;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
.md-inline-code {
|
|
1145
|
+
padding: 1px 5px;
|
|
1146
|
+
border-radius: 4px;
|
|
1147
|
+
background: var(--neutral-soft);
|
|
1148
|
+
font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1149
|
+
overflow-wrap: anywhere;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
.md-link {
|
|
1153
|
+
color: var(--accent);
|
|
1154
|
+
overflow-wrap: anywhere;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
.md-strong {
|
|
1158
|
+
font-weight: 600;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
.md-del {
|
|
1162
|
+
color: var(--muted);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
.md-table {
|
|
1166
|
+
display: block;
|
|
1167
|
+
width: max-content;
|
|
1168
|
+
max-width: 100%;
|
|
1169
|
+
margin: 0 0 8px;
|
|
1170
|
+
border-collapse: collapse;
|
|
1171
|
+
overflow-x: auto;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
.md-table th,
|
|
1175
|
+
.md-table td {
|
|
1176
|
+
padding: 5px 10px;
|
|
1177
|
+
border: 1px solid var(--border-soft);
|
|
1178
|
+
text-align: left;
|
|
1179
|
+
vertical-align: top;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
.md-table th {
|
|
1183
|
+
background: var(--neutral-soft);
|
|
1184
|
+
font-weight: 600;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1006
1187
|
.message-foot {
|
|
1007
1188
|
display: flex;
|
|
1008
1189
|
align-items: center;
|
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.16";
|
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.16";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imenam/mcp-messenger",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"main": "dist/src/index.js",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "npm run build && vitest run",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"@types/express": "^5.0.6",
|
|
50
50
|
"@types/node": "^22.20.1",
|
|
51
51
|
"@vitest/coverage-v8": "^3.2.7",
|
|
52
|
+
"jsdom": "^29.1.1",
|
|
52
53
|
"typescript": "^5.9.3",
|
|
53
54
|
"vitest": "^3.2.7"
|
|
54
55
|
},
|