fix(console): open at bottom, drop Disconnected from transcript, add jump-to-latest button

- Stop persisting "Disconnected" / "Failed to connect" — these are session-local events; saving them made every reopen begin with a stale red error.
- Scroll to the bottom after restoring transcript so reopens land at the latest entry instead of the top.
- Add a floating chat-style jump-to-latest button that appears whenever the user scrolls more than ~80px above the bottom and disappears once they're back at the latest entry.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-05-06 07:42:51 +02:00
parent 3ef1eac0be
commit a571d5388d
2 changed files with 73 additions and 4 deletions
+31 -2
View File
@@ -22,6 +22,7 @@ document.addEventListener('DOMContentLoaded', async function() {
setupInputHandlers();
setupHistoryDropdown();
setupClearOutputButton();
setupScrollToBottom();
});
/**
@@ -58,7 +59,8 @@ function connectWebSocket() {
isConnected = false;
updateStatus('disconnected');
enableInput(false);
addMessage('Disconnected', 'error');
// Transient session event — show inline but don't persist to transcript
addMessage('Disconnected', 'error', false);
// Clear pending command indicator
if (pendingCommandDiv) {
@@ -102,7 +104,7 @@ function connectWebSocket() {
} catch (error) {
console.error('Failed to create WebSocket connection:', error);
updateStatus('disconnected');
addMessage('Failed to connect: ' + error.message, 'error');
addMessage('Failed to connect: ' + error.message, 'error', false);
}
}
@@ -456,6 +458,8 @@ async function loadOutputHistory() {
const divider = document.createElement('hr');
divider.className = 'history-divider';
container.appendChild(divider);
// Open at the bottom of the transcript, like a chat window
scrollToBottom();
} catch (error) {
console.error('Failed to load output history:', error);
}
@@ -502,3 +506,28 @@ function setupClearOutputButton() {
clearOutputHistory();
});
}
/**
* Wire the floating scroll-to-bottom button: visible when the user has
* scrolled away from the bottom of the transcript.
*/
function setupScrollToBottom() {
const container = document.getElementById('consoleMessages');
const btn = document.getElementById('scrollBottomBtn');
if (!container || !btn) return;
const SHOW_THRESHOLD = 80; // px from bottom before button appears
const update = () => {
const distance = container.scrollHeight - container.scrollTop - container.clientHeight;
btn.classList.toggle('show', distance > SHOW_THRESHOLD);
};
container.addEventListener('scroll', update, { passive: true });
// Re-check after content changes (new messages, dropdowns, etc.)
new MutationObserver(update).observe(container, { childList: true, subtree: false });
btn.addEventListener('click', (e) => {
e.preventDefault();
container.scrollTop = container.scrollHeight;
});
}
+42 -2
View File
@@ -44,6 +44,13 @@
font-weight: 500;
}
.messages-wrap {
flex: 1;
min-height: 0;
position: relative;
display: flex;
}
.console-messages {
flex: 1;
overflow-y: auto;
@@ -52,6 +59,34 @@
min-height: 0;
}
.scroll-bottom-btn {
position: absolute;
right: 1rem;
bottom: 1rem;
width: 36px;
height: 36px;
border-radius: 50%;
border: 1px solid #0f3460;
background-color: #16213e;
color: #4ecdc4;
display: none;
align-items: center;
justify-content: center;
padding: 0;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
cursor: pointer;
z-index: 5;
}
.scroll-bottom-btn.show {
display: flex;
}
.scroll-bottom-btn:hover {
background-color: #1a1a4e;
color: #00ff88;
}
.console-message {
margin-bottom: 1rem;
font-family: 'Courier New', Consolas, monospace;
@@ -277,8 +312,13 @@
</div>
<!-- Messages Area -->
<div class="console-messages" id="consoleMessages">
<div class="console-message system">Type 'help' for available commands.</div>
<div class="messages-wrap">
<div class="console-messages" id="consoleMessages">
<div class="console-message system">Type 'help' for available commands.</div>
</div>
<button type="button" class="scroll-bottom-btn" id="scrollBottomBtn" title="Scroll to latest" aria-label="Scroll to latest">
<i class="bi bi-arrow-down"></i>
</button>
</div>
<!-- Input Area -->