Monitor conversation list: collapsible section for the card's own profile above the full list

A flat list buries 'where was I in General Chat' under whatever the tabs have been doing, since the busiest profile takes the top regardless of which card you are looking at.
This commit is contained in:
Gmer4Lfe
2026-08-17 15:45:14 -04:00
parent d3d6f30f72
commit 83f3b7e226
2 changed files with 70 additions and 14 deletions
+64 -15
View File
@@ -2163,20 +2163,21 @@ vv_ai_profiles_script();
if (!box) return null;
let rows = [], activeId = '';
function render() {
if (!rows.length) {
box.innerHTML = '<div class="vv-ai-none">no saved conversations yet</div>';
return;
}
// The scope is shown, not just stored. A Scheduler thread is about one script or log,
// and a list of titles alone makes "why does this one talk about a log I never opened"
// an unanswerable question.
// Tagged only when the tag says something. A scope always does; a profile does unless it
// is the ordinary one — labelling every General Chat row "Chat" is a column of noise that
// makes the rows that are genuinely different harder to pick out, which is the opposite
// of the point.
const P = window.VvAiProfiles || {};
box.innerHTML = '<div class="vv-ai-clist">' + rows.map(c => {
// Optional first section: this card's own profile, collapsed, holding its most recent
// conversations. Underneath it the full list carries on unchanged.
//
// The two answer different questions. "Where was I in General Chat" is the one asked on a
// card pinned to General Chat, and a single flat list buries it under whatever the tabs have
// been doing — the profile with the most threads wins the top of the list regardless of which
// card you are looking at. Collapsed by default because the card resumes the newest of these
// anyway; opening it is for reaching the other nine.
const GROUP = o.groupProfile || '';
const GROUP_MAX = o.groupMax || 10;
const OPEN_KEY = 'vvAiListOpen:' + (o.into || '');
let groupOpen = false;
try { groupOpen = localStorage.getItem(OPEN_KEY) === '1'; } catch (_) {}
function rowHtml(c, P) {
const prof = (c.profile && c.profile !== 'chat' && P[c.profile]) ? P[c.profile].short : '';
const tag = [prof, c.scope || ''].filter(Boolean).join(' · ');
return `<div class="vv-ai-crow${c.id === activeId ? ' active' : ''}" data-id="${esc(c.id)}">`
@@ -2187,7 +2188,47 @@ vv_ai_profiles_script();
+ `<span class="vv-ai-crow-m">${esc(ago(c.ts))}</span>`
+ `<span class="vv-ai-crow-x" data-del="${esc(c.id)}" title="Delete">×</span>`
+ `</div>`;
}).join('') + '</div>';
}
function groupHtml() {
if (!GROUP) return '';
const P = window.VvAiProfiles || {};
const name = (P[GROUP] && P[GROUP].short) || (GROUP === 'chat' ? 'General Chat' : GROUP);
const mine = rows.filter(c => (c.profile || 'chat') === GROUP)
.sort((a, b) => (b.updated || b.ts || 0) - (a.updated || a.ts || 0))
.slice(0, GROUP_MAX);
return `<div class="vv-ai-cgroup">`
+ `<div class="vv-ai-chead" data-grp="1" style="display:flex;align-items:center;gap:6px;
cursor:pointer;padding:3px 4px;user-select:none;">`
+ `<span style="color:#666;font-size:10px;">${groupOpen ? '▾' : '▸'}</span>`
+ `<span style="color:#888;font-size:10px;font-weight:600;">${esc(name)}</span>`
+ `<span style="color:#333;font-size:9px;">${mine.length}</span>`
+ `</div>`
+ (groupOpen
? `<div class="vv-ai-clist">`
+ (mine.length ? mine.map(c => rowHtml(c, P)).join('')
: '<div class="vv-ai-none">none yet</div>')
+ `</div>`
: '')
+ `<div style="border-bottom:1px solid #1b1b1b;margin:5px 0;"></div>`
+ `</div>`;
}
function render() {
if (!rows.length) {
box.innerHTML = groupHtml() + '<div class="vv-ai-none">no saved conversations yet</div>';
return;
}
// The scope is shown, not just stored. A Scheduler thread is about one script or log,
// and a list of titles alone makes "why does this one talk about a log I never opened"
// an unanswerable question.
// Tagged only when the tag says something. A scope always does; a profile does unless it
// is the ordinary one — labelling every General Chat row "Chat" is a column of noise that
// makes the rows that are genuinely different harder to pick out, which is the opposite
// of the point.
const P = window.VvAiProfiles || {};
box.innerHTML = groupHtml()
+ '<div class="vv-ai-clist">' + rows.map(c => rowHtml(c, P)).join('') + '</div>';
}
function load() {
@@ -2197,6 +2238,14 @@ vv_ai_profiles_script();
}
box.addEventListener('click', e => {
// Before the row handler: the header sits above the rows and must not be read as one.
const grp = e.target.closest('[data-grp]');
if (grp) {
groupOpen = !groupOpen;
try { localStorage.setItem(OPEN_KEY, groupOpen ? '1' : '0'); } catch (_) {}
render();
return;
}
const del = e.target.closest('[data-del]');
if (del) {
e.stopPropagation();
+8 -1
View File
@@ -2867,7 +2867,14 @@ if (document.getElementById('vv-mon-ai-chat')) {
// for the poll to reflect it reads as the card being broken.
onTurn: () => vvLoadTokens(),
});
vvMonChatList = VvAiChatList({ into: 'vv-mon-ai-chats', chat: vvMonChat });
vvMonChatList = VvAiChatList({
into: 'vv-mon-ai-chats',
chat: vvMonChat,
// This card is pinned to General Chat, so its own threads get the top section rather than
// competing for position with everything the tabs have been doing.
groupProfile: 'chat',
groupMax: 10,
});
vvMonChatList.reload();
// Re-measured rather than computed once. The Assistant changes height when the transcript is