Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions extension/demo/chat-poc/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
// =============================================================================
Expand All @@ -39,6 +45,7 @@ let messages = [];
let useTools = true;
let isProcessing = false;
let availableTools = [];
let stickToBottom = true;

// =============================================================================
// DOM Elements
Expand Down Expand Up @@ -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
// =============================================================================
Expand Down Expand Up @@ -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();
Expand Down
68 changes: 59 additions & 9 deletions extension/demo/chat-poc/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -540,17 +582,25 @@
</div>
</div>

<div class="chat-container" id="chat-container">
<div class="messages" id="messages">
<div class="empty-state" id="empty-state">
<div class="empty-state-icon">💬</div>
<h2>Harbor Chat</h2>
<p>
Test chat interface for debugging.<br>
Toggle <strong>Tools</strong> to use MCP tools.
</p>
<div class="chat-area">
<div class="chat-container" id="chat-container">
<div class="messages" id="messages">
<div class="empty-state" id="empty-state">
<div class="empty-state-icon">💬</div>
<h2>Harbor Chat</h2>
<p>
Test chat interface for debugging.<br>
Toggle <strong>Tools</strong> to use MCP tools.
</p>
</div>
</div>
</div>
<button class="jump-to-latest" id="jump-to-latest" type="button" aria-label="Jump to latest message">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 5v14M19 12l-7 7-7-7"/>
</svg>
<span>Jump to latest</span>
</button>
</div>

<div class="input-area">
Expand Down