Hold Conversations and Tokens to the Assistant's height

A stretched grid item is measured before it is stretched, so a long card grew the row it was
meant to fit inside and then filled it; the ceiling is taken from the Assistant instead.
This commit is contained in:
Gmer4Lfe
2026-08-09 11:09:04 -04:00
parent 7b1b017d8c
commit 4f598f7422
2 changed files with 52 additions and 1 deletions
+45
View File
@@ -2087,6 +2087,36 @@ function vvRenderTokens() {
box.innerHTML = h;
}
// ── AI row height ────────────────────────────────────────────────────────────
// Conversations and Tokens are capped to the Assistant and scroll inside that cap. CSS cannot
// state this on its own: align-self:stretch makes an item fill its grid track, but the track was
// sized from the item's own content first, so a card with a long list simply grows the row and
// then dutifully fills the bigger row. Measuring the Assistant breaks that circle — it is the
// only card in the row whose height is decided by nothing but itself.
//
// Applied only in the eight-column layout. Below 1025px the Assistant drops to a row of its own
// and stops being what these two sit beside, so a cap taken from it would describe nothing.
function vvAiRowSync() {
const asst = document.getElementById('vv-ai-assistant-card');
const chats = document.getElementById('vv-ai-chats-card');
const toks = document.getElementById('vv-ai-tokens-card');
const stats = document.getElementById('vv-ai-stats-card');
if (!asst || !chats) return;
if (!window.matchMedia('(min-width: 1025px)').matches) {
chats.style.maxHeight = '';
if (toks) toks.style.maxHeight = '';
return;
}
const h = asst.offsetHeight;
chats.style.maxHeight = h + 'px';
// Tokens is in the second row, under AI — its share is what the Assistant leaves once the
// first row and the 12px gap are taken. Floored so it stays a card rather than a sliver on a
// short viewport where the AI card happens to be tall.
if (toks && stats) toks.style.maxHeight = Math.max(140, h - stats.offsetHeight - 12) + 'px';
}
// Delegated on the container, which survives every render — the rows inside do not, and binding
// per row would rebind the whole list on each poll to no purpose.
if (document.getElementById('vv-ai-tokens-body')) {
@@ -2822,5 +2852,20 @@ if (document.getElementById('vv-mon-ai-chat')) {
});
vvMonChatList = VvAiChatList({ into: 'vv-mon-ai-chats', chat: vvMonChat });
vvMonChatList.reload();
// Re-measured rather than computed once. The Assistant changes height when the transcript is
// expanded, and the AI card above changes when its first real payload replaces "Loading...";
// both move the ceiling these two cards sit under. Observing the two sources covers each
// without watching the cards being adjusted, so there is no feedback loop. The resize listener
// is for crossing the 1025px breakpoint, which changes whether a cap applies at all.
vvAiRowSync();
if (window.ResizeObserver) {
const ro = new ResizeObserver(vvAiRowSync);
['vv-ai-assistant-card', 'vv-ai-stats-card'].forEach(id => {
const el = document.getElementById(id);
if (el) ro.observe(el);
});
}
window.addEventListener('resize', vvAiRowSync);
}
</script>