diff --git a/hive-ag3nt/assets/agent.css b/hive-ag3nt/assets/agent.css index bb7818d..ca912c2 100644 --- a/hive-ag3nt/assets/agent.css +++ b/hive-ag3nt/assets/agent.css @@ -130,6 +130,7 @@ pre.diff { so anything that bleeds through (page banner glow, scroll position) reads as out-of-focus depth instead of sharp competing detail. */ .terminal-wrap { + position: relative; background: rgba(17, 17, 27, 0.78); -webkit-backdrop-filter: blur(8px) saturate(120%); backdrop-filter: blur(8px) saturate(120%); @@ -227,6 +228,36 @@ pre.diff { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } } +/* "↓ N new" pill: shown when new rows arrive while the operator is + scrolled up; click to jump to bottom. */ +.tail-pill { + position: absolute; + right: 1em; + bottom: 4.2em; + background: var(--amber); + color: #11111b; + font-family: inherit; + font-size: 0.8em; + font-weight: bold; + letter-spacing: 0.08em; + border: 0; + border-radius: 999px; + padding: 0.35em 0.9em; + cursor: pointer; + box-shadow: 0 0 14px -2px rgba(250, 179, 135, 0.85); + opacity: 0; + transform: translateY(6px); + pointer-events: none; + transition: opacity 160ms ease, transform 160ms ease; +} +.tail-pill.visible { + opacity: 1; + transform: translateY(0); + pointer-events: auto; +} +.tail-pill:hover { + filter: brightness(1.1); +} details.row { white-space: normal; padding-left: 0.5em; diff --git a/hive-ag3nt/assets/app.js b/hive-ag3nt/assets/app.js index 45c1bdc..763526c 100644 --- a/hive-ag3nt/assets/app.js +++ b/hive-ag3nt/assets/app.js @@ -331,13 +331,58 @@ row: (cls, text) => row(cls, text), clear: () => { log.innerHTML = ''; placeholder = null; }, }; + + // Sticky-bottom auto-scroll. If the user is reading scrolled-up, new + // rows do NOT yank the view. A floating "↓ N new" pill appears in + // the bottom-right corner; clicking it jumps to bottom and clears + // the counter. Scrolling back near the bottom also clears it. + const NEAR_BOTTOM_PX = 48; + let unseen = 0; + let pill = null; + function isNearBottom() { + return log.scrollHeight - log.scrollTop - log.clientHeight <= NEAR_BOTTOM_PX; + } + function ensurePill() { + if (pill) return pill; + pill = document.createElement('button'); + pill.type = 'button'; + pill.className = 'tail-pill'; + pill.addEventListener('click', () => { + log.scrollTop = log.scrollHeight; + }); + log.parentElement.appendChild(pill); + return pill; + } + function updatePill() { + if (unseen <= 0) { + if (pill) pill.classList.remove('visible'); + return; + } + ensurePill(); + pill.textContent = '↓ ' + unseen + ' new'; + pill.classList.add('visible'); + } + log.addEventListener('scroll', () => { + if (isNearBottom()) { + unseen = 0; + updatePill(); + } + }); + function afterAppend() { + if (currentNoAnim || isNearBottom()) { + log.scrollTop = log.scrollHeight; + } else { + unseen += 1; + updatePill(); + } + } function row(cls, text) { clearPlaceholder(); const e = document.createElement('div'); e.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : ''); e.textContent = text; log.appendChild(e); - log.scrollTop = log.scrollHeight; + afterAppend(); return e; } function details(cls, summary, body) { @@ -352,7 +397,7 @@ pre.textContent = body; d.appendChild(pre); log.appendChild(d); - log.scrollTop = log.scrollHeight; + afterAppend(); return d; } function trim(s, n) { return s.length > n ? s.slice(0, n) + '…' : s; }