From d6462b32503750dde12339459288e796f1fcb72e Mon Sep 17 00:00:00 2001 From: Bobby Richter <403231+secretrobotron@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:41:25 +0200 Subject: [PATCH] feature: Gating for auto-scrolling in POC chat window when LLM response is inbound Added structure for gating scrollToBottom on whether or not user is already at bottom of chat window, or jumps there explicitly. --- extension/demo/chat-poc/app.js | 50 +++++++++++++++++++++- extension/demo/chat-poc/index.html | 68 ++++++++++++++++++++++++++---- 2 files changed, 107 insertions(+), 11 deletions(-) diff --git a/extension/demo/chat-poc/app.js b/extension/demo/chat-poc/app.js index 2eb038a..a76e134 100644 --- a/extension/demo/chat-poc/app.js +++ b/extension/demo/chat-poc/app.js @@ -30,6 +30,12 @@ * See README.md and the Web Agent API spec for more details. */ +// ============================================================================= +// Constants +// ============================================================================= + +const BOTTOM_STICKY_THRESHOLD = 8; // Pixels from bottom to auto-stick + // ============================================================================= // State // ============================================================================= @@ -39,6 +45,7 @@ let messages = []; let useTools = true; let isProcessing = false; let availableTools = []; +let stickToBottom = true; // ============================================================================= // DOM Elements @@ -66,6 +73,8 @@ const toolsModalClose = document.getElementById('tools-modal-close'); const toolsModalContent = document.getElementById('tools-modal-content'); const toolsModalCount = document.getElementById('tools-modal-count'); +const jumpToLatestBtn = document.getElementById('jump-to-latest'); + // ============================================================================= // Status Checking // ============================================================================= @@ -336,16 +345,53 @@ function removeThinking() { if (el) el.remove(); } -function scrollToBottom() { +function scrollToBottom(force = false) { + if (!force && !stickToBottom) return; chatContainer.scrollTop = chatContainer.scrollHeight; } +function setStickToBottom(nowStuck) { + if (nowStuck === stickToBottom) return; + stickToBottom = nowStuck; + jumpToLatestBtn.classList.toggle('visible', !nowStuck); +} + +chatContainer.addEventListener('scroll', () => { + const distance = chatContainer.scrollHeight - chatContainer.scrollTop - chatContainer.clientHeight; + setStickToBottom(distance < BOTTOM_STICKY_THRESHOLD); +}, { passive: true }); + +function hasScrollableContent() { + return chatContainer.scrollHeight > chatContainer.clientHeight; +} + +// Catch user intent BEFORE the scroll event lands — otherwise a token arriving +// in the same tick can call scrollToBottom() and clobber the user's wheel before +// our scroll listener ever sees the new position. +chatContainer.addEventListener('wheel', (e) => { + if (e.deltaY < 0 && hasScrollableContent()) setStickToBottom(false); +}, { passive: true }); + +chatContainer.addEventListener('keydown', (e) => { + if ((e.key === 'ArrowUp' || e.key === 'PageUp' || e.key === 'Home') && hasScrollableContent()) { + setStickToBottom(false); + } +}); + +jumpToLatestBtn.addEventListener('click', () => { + scrollToBottom(true); + setStickToBottom(true); +}); + function clearChat() { messages.length = 0; messagesEl.innerHTML = ''; messagesEl.appendChild(emptyState); emptyState.style.display = 'flex'; - + + stickToBottom = true; + jumpToLatestBtn.classList.remove('visible'); + // Destroy and reset session if (session) { session.destroy(); diff --git a/extension/demo/chat-poc/index.html b/extension/demo/chat-poc/index.html index 5ce17b3..aba2ab1 100644 --- a/extension/demo/chat-poc/index.html +++ b/extension/demo/chat-poc/index.html @@ -95,10 +95,52 @@ .status-dot.error { background: var(--color-error); } /* Chat container */ + .chat-area { + flex: 1; + position: relative; + display: flex; + flex-direction: column; + min-height: 0; + } + .chat-container { flex: 1; overflow-y: auto; padding: var(--space-4); + min-height: 0; + } + + .jump-to-latest { + position: absolute; + bottom: var(--space-3); + left: 50%; + transform: translateX(-50%) translateY(8px); + background: var(--color-accent-primary); + color: white; + border: none; + border-radius: var(--radius-full); + padding: 6px 14px; + font-size: var(--text-xs); + font-weight: var(--weight-medium); + cursor: pointer; + display: inline-flex; + align-items: center; + gap: var(--space-1); + box-shadow: var(--shadow-sm); + opacity: 0; + pointer-events: none; + transition: opacity var(--transition-fast), transform var(--transition-fast), background var(--transition-fast); + z-index: 1; + } + + .jump-to-latest:hover { + background: var(--color-accent-primary-hover); + } + + .jump-to-latest.visible { + opacity: 1; + pointer-events: auto; + transform: translateX(-50%) translateY(0); } .messages { @@ -540,17 +582,25 @@ -
-
-
-
💬
-

Harbor Chat

-

- Test chat interface for debugging.
- Toggle Tools to use MCP tools. -

+
+
+
+
+
💬
+

Harbor Chat

+

+ Test chat interface for debugging.
+ Toggle Tools to use MCP tools. +

+
+