diff --git a/internal/loki/ui/index.html b/internal/loki/ui/index.html
index 3a9516f..fcf91fa 100644
--- a/internal/loki/ui/index.html
+++ b/internal/loki/ui/index.html
@@ -1746,6 +1746,9 @@ html[data-files="1"] #files-btn{color:var(--accent)}
color:var(--dim);letter-spacing:-.01em;overflow:visible}
.side .preset:hover .conv-when{opacity:0}
.conv-empty{font-size:12px;color:var(--dim);padding:8px 11px}
+.conv-more{display:block;width:100%;font:inherit;font-size:12px;color:var(--dim);background:none;
+ border:0;padding:8px 11px;text-align:left;cursor:pointer}
+.conv-more:hover{color:var(--text)}
/* Discussion au travail : un anneau qui tourne, à gauche du titre. C'est le seul
élément animé de la liste — il désigne sans ambiguïté le fil sur lequel un
agent est en train de travailler, y compris depuis un autre appareil. */
@@ -9127,6 +9130,12 @@ let CONVS = [], CONV_ACTIVE = '';
// ligne doit déjà s'animer si un agent travaille (page rechargée en cours de
// route, ou ouverte depuis un autre appareil).
let CONV_BUSY = false;
+// Liste paginée (AJEAN 0.15.6) : avec des centaines de discussions, construire
+// toutes les lignes d'un coup figeait l'ouverture du panneau. On en dessine
+// CONV_PAGE, puis la suite quand le bas de la liste devient visible. La
+// recherche, elle, filtre toujours TOUTE la liste (CONVS est en mémoire).
+const CONV_PAGE = 40;
+let CONV_SHOWN = CONV_PAGE, CONV_LAST_Q = '', convMoreObs = null;
async function loadConversations(){
let r;
@@ -9149,6 +9158,7 @@ function renderConversations(){
if(!cont) return;
const q = (document.getElementById('conv-search')?.value || '').trim().toLowerCase();
cont.textContent = '';
+ if(q !== CONV_LAST_Q){ CONV_LAST_Q = q; CONV_SHOWN = CONV_PAGE; }
const list = q
? CONVS.filter(c => (c.title || 'Nouvelle discussion').toLowerCase().includes(q))
: CONVS;
@@ -9161,7 +9171,10 @@ function renderConversations(){
convSyncBusy();
return;
}
- for(const c of list){
+ // La discussion active reste toujours visible, même loin dans la liste.
+ const ai = list.findIndex(c => c.id === CONV_ACTIVE);
+ const shown = Math.max(CONV_SHOWN, ai + 1);
+ for(const c of list.slice(0, shown)){
const row = document.createElement('div');
row.className = 'preset' + (c.id === CONV_ACTIVE ? ' active' : '');
// L'identifiant est porté par la ligne : convSyncBusy retrouve ainsi CELLE
@@ -9199,10 +9212,28 @@ function renderConversations(){
row.append(info, when, acts);
cont.appendChild(row);
}
+ if(list.length > shown) convMoreRow(cont, list.length - shown);
syncTopbarTitle();
convSyncBusy();
}
+// Ligne « N de plus » en bas de la liste : un clic, ou son arrivée à l'écran
+// (IntersectionObserver), dessine la page suivante.
+function convMoreRow(cont, left){
+ const more = document.createElement('button');
+ more.type = 'button';
+ more.className = 'conv-more';
+ more.textContent = 'afficher ' + Math.min(left, CONV_PAGE) + ' de plus (' + left + ' restantes)';
+ const next = () => { CONV_SHOWN += CONV_PAGE; renderConversations(); };
+ more.onclick = next;
+ cont.appendChild(more);
+ if(convMoreObs) convMoreObs.disconnect();
+ if(window.IntersectionObserver){
+ convMoreObs = new IntersectionObserver(es => { if(es.some(e => e.isIntersecting)){ convMoreObs.disconnect(); next(); } });
+ convMoreObs.observe(more);
+ }
+}
+
// --- Discussion au travail : anneau qui tourne ------------------------------
// Une seule génération tourne à la fois, et c'est forcément sur la discussion
// OUVERTE (le serveur n'a qu'un fil en mémoire) : l'indicateur va donc sur la
diff --git a/internal/loki/ui/src/js/16-conversations.js b/internal/loki/ui/src/js/16-conversations.js
index f5103ae..48dd777 100644
--- a/internal/loki/ui/src/js/16-conversations.js
+++ b/internal/loki/ui/src/js/16-conversations.js
@@ -19,6 +19,12 @@ let CONVS = [], CONV_ACTIVE = '';
// ligne doit déjà s'animer si un agent travaille (page rechargée en cours de
// route, ou ouverte depuis un autre appareil).
let CONV_BUSY = false;
+// Liste paginée (AJEAN 0.15.6) : avec des centaines de discussions, construire
+// toutes les lignes d'un coup figeait l'ouverture du panneau. On en dessine
+// CONV_PAGE, puis la suite quand le bas de la liste devient visible. La
+// recherche, elle, filtre toujours TOUTE la liste (CONVS est en mémoire).
+const CONV_PAGE = 40;
+let CONV_SHOWN = CONV_PAGE, CONV_LAST_Q = '', convMoreObs = null;
async function loadConversations(){
let r;
@@ -41,6 +47,7 @@ function renderConversations(){
if(!cont) return;
const q = (document.getElementById('conv-search')?.value || '').trim().toLowerCase();
cont.textContent = '';
+ if(q !== CONV_LAST_Q){ CONV_LAST_Q = q; CONV_SHOWN = CONV_PAGE; }
const list = q
? CONVS.filter(c => (c.title || 'Nouvelle discussion').toLowerCase().includes(q))
: CONVS;
@@ -53,7 +60,10 @@ function renderConversations(){
convSyncBusy();
return;
}
- for(const c of list){
+ // La discussion active reste toujours visible, même loin dans la liste.
+ const ai = list.findIndex(c => c.id === CONV_ACTIVE);
+ const shown = Math.max(CONV_SHOWN, ai + 1);
+ for(const c of list.slice(0, shown)){
const row = document.createElement('div');
row.className = 'preset' + (c.id === CONV_ACTIVE ? ' active' : '');
// L'identifiant est porté par la ligne : convSyncBusy retrouve ainsi CELLE
@@ -91,10 +101,28 @@ function renderConversations(){
row.append(info, when, acts);
cont.appendChild(row);
}
+ if(list.length > shown) convMoreRow(cont, list.length - shown);
syncTopbarTitle();
convSyncBusy();
}
+// Ligne « N de plus » en bas de la liste : un clic, ou son arrivée à l'écran
+// (IntersectionObserver), dessine la page suivante.
+function convMoreRow(cont, left){
+ const more = document.createElement('button');
+ more.type = 'button';
+ more.className = 'conv-more';
+ more.textContent = 'afficher ' + Math.min(left, CONV_PAGE) + ' de plus (' + left + ' restantes)';
+ const next = () => { CONV_SHOWN += CONV_PAGE; renderConversations(); };
+ more.onclick = next;
+ cont.appendChild(more);
+ if(convMoreObs) convMoreObs.disconnect();
+ if(window.IntersectionObserver){
+ convMoreObs = new IntersectionObserver(es => { if(es.some(e => e.isIntersecting)){ convMoreObs.disconnect(); next(); } });
+ convMoreObs.observe(more);
+ }
+}
+
// --- Discussion au travail : anneau qui tourne ------------------------------
// Une seule génération tourne à la fois, et c'est forcément sur la discussion
// OUVERTE (le serveur n'a qu'un fil en mémoire) : l'indicateur va donc sur la
diff --git a/internal/loki/ui/src/styles.css b/internal/loki/ui/src/styles.css
index 6128a48..22c6fde 100644
--- a/internal/loki/ui/src/styles.css
+++ b/internal/loki/ui/src/styles.css
@@ -1711,6 +1711,9 @@ html[data-files="1"] #files-btn{color:var(--accent)}
color:var(--dim);letter-spacing:-.01em;overflow:visible}
.side .preset:hover .conv-when{opacity:0}
.conv-empty{font-size:12px;color:var(--dim);padding:8px 11px}
+.conv-more{display:block;width:100%;font:inherit;font-size:12px;color:var(--dim);background:none;
+ border:0;padding:8px 11px;text-align:left;cursor:pointer}
+.conv-more:hover{color:var(--text)}
/* Discussion au travail : un anneau qui tourne, à gauche du titre. C'est le seul
élément animé de la liste — il désigne sans ambiguïté le fil sur lequel un
agent est en train de travailler, y compris depuis un autre appareil. */