fix: streaming scroll override, Gemini 3.x models, read-only workspace, two-container UID — v0.50.87 (closes #677 #669 #670 #668)

- #677: renderMessages() and appendThinking() use scrollIfPinned() during stream; scroll threshold 80→150px; floating ↓ scroll-to-bottom button added
- #669: Gemini 3.1 Pro Preview, 3 Flash Preview, 3.1 Flash Lite Preview added to all provider sections; gemini-3.1-flash-lite-preview was the missing ID causing API_KEY_INVALID; GEMINI_API_KEY env var detection added
- #670: docker_init.bash guards chown/write-test with [ -w ]; :ro workspace mounts no longer crash startup
- #668: UID/GID auto-detect probes /home/hermeswebui/.hermes and HERMES_HOME before /workspace; two-container Zeabur/Compose setups inherit correct UID automatically
- 18 new tests; 1441 total passing
This commit is contained in:
nesquena-hermes
2026-04-18 10:09:59 -07:00
committed by GitHub
parent 5266ee26bd
commit 352354790f
11 changed files with 401 additions and 41 deletions

View File

@@ -180,6 +180,7 @@
</div>
</div>
<div class="messages" id="messages">
<button id="scrollToBottomBtn" class="scroll-to-bottom-btn" aria-label="Scroll to bottom" onclick="scrollToBottom()" style="display:none"></button>
<div class="empty-state" id="emptyState">
<div class="empty-logo"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="80" height="80" aria-label="Hermes caduceus">
<defs>
@@ -329,7 +330,8 @@
<option value="anthropic/claude-haiku-3-5">Claude Haiku 3.5</option>
</optgroup>
<optgroup label="Other">
<option value="google/gemini-2.5-pro">Gemini 2.5 Pro</option>
<option value="google/gemini-3.1-pro-preview">Gemini 3.1 Pro Preview</option>
<option value="google/gemini-3-flash-preview">Gemini 3 Flash Preview</option>
<option value="deepseek/deepseek-chat-v3-0324">DeepSeek V3</option>
<option value="meta-llama/llama-4-scout">Llama 4 Scout</option>
</optgroup>
@@ -592,7 +594,7 @@
<div class="settings-section-title">System</div>
<div class="settings-section-meta">Instance version and access controls.</div>
</div>
<span class="settings-version-badge">v0.50.86</span>
<span class="settings-version-badge">v0.50.87</span>
</div>
<div class="settings-field" style="border-top:1px solid var(--border);padding-top:12px;margin-top:8px">
<label for="settingsPassword" data-i18n="settings_label_password">Access Password</label>

View File

@@ -428,6 +428,9 @@
.workspace-toggle-btn:disabled{opacity:.38;cursor:not-allowed;}
.chip.model{color:var(--accent-text);border-color:var(--accent-bg-strong);background:var(--accent-bg);}
.messages{flex:1;overflow-y:auto;display:flex;flex-direction:column;min-height:0;position:relative;z-index:0;-webkit-overflow-scrolling:touch;touch-action:pan-y;overscroll-behavior-y:contain;}
/* sticky-first-child: button is first child of .messages so its natural position is above viewport; sticky+bottom:16px pins it there when visible */
.scroll-to-bottom-btn{position:sticky;bottom:16px;align-self:flex-end;margin-right:20px;width:32px;height:32px;border-radius:50%;border:1px solid var(--border2);background:var(--code-bg);color:var(--muted);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,.25);z-index:10;transition:color .12s,border-color .12s,background .12s;}
.scroll-to-bottom-btn:hover{color:var(--text);border-color:var(--border);background:var(--hover-bg);}
.messages-inner{margin:0 auto;width:100%;padding:20px 24px 32px;display:flex;flex-direction:column;}
@media(min-width:1400px){.messages-inner{max-width:1100px;}}
@media(min-width:1800px){.messages-inner{max-width:1200px;}}

View File

@@ -369,14 +369,16 @@ window.addEventListener('resize',()=>{
// ── Scroll pinning ──────────────────────────────────────────────────────────
// When streaming, auto-scroll only if the user hasn't manually scrolled up.
// Once the user scrolls back to within 80px of the bottom, re-pin.
// Once the user scrolls back to within 150px of the bottom, re-pin.
let _scrollPinned=true;
(function(){
const el=document.getElementById('messages');
if(!el) return;
el.addEventListener('scroll',()=>{
const nearBottom=el.scrollHeight-el.scrollTop-el.clientHeight<80;
const nearBottom=el.scrollHeight-el.scrollTop-el.clientHeight<150;
_scrollPinned=nearBottom;
const btn=$('scrollToBottomBtn');
if(btn) btn.style.display=_scrollPinned?'none':'flex';
});
})();
function _fmtTokens(n){if(!n||n<0)return'0';if(n>=1e6)return(n/1e6).toFixed(1)+'M';if(n>=1e3)return(n/1e3).toFixed(1)+'k';return String(n);}
@@ -447,6 +449,8 @@ function scrollToBottom(){
_scrollPinned=true;
const el=$('messages');
if(el) el.scrollTop=el.scrollHeight;
const btn=$('scrollToBottomBtn');
if(btn) btn.style.display='none';
}
function getModelLabel(modelId){
@@ -454,7 +458,7 @@ function getModelLabel(modelId){
// Check dynamic labels first, then fall back to splitting the ID
if(_dynamicModelLabels[modelId]) return _dynamicModelLabels[modelId];
// Static fallback for common models
const STATIC_LABELS={'openai/gpt-5.4-mini':'GPT-5.4 Mini','openai/gpt-4o':'GPT-4o','openai/o3':'o3','openai/o4-mini':'o4-mini','anthropic/claude-sonnet-4.6':'Sonnet 4.6','anthropic/claude-sonnet-4-5':'Sonnet 4.5','anthropic/claude-haiku-3-5':'Haiku 3.5','google/gemini-2.5-pro':'Gemini 2.5 Pro','deepseek/deepseek-chat-v3-0324':'DeepSeek V3','meta-llama/llama-4-scout':'Llama 4 Scout'};
const STATIC_LABELS={'openai/gpt-5.4-mini':'GPT-5.4 Mini','openai/gpt-4o':'GPT-4o','openai/o3':'o3','openai/o4-mini':'o4-mini','anthropic/claude-sonnet-4.6':'Sonnet 4.6','anthropic/claude-sonnet-4-5':'Sonnet 4.5','anthropic/claude-haiku-3-5':'Haiku 3.5','google/gemini-3.1-pro-preview':'Gemini 3.1 Pro','google/gemini-3-flash-preview':'Gemini 3 Flash','google/gemini-3.1-flash-lite-preview':'Gemini 3.1 Flash Lite','google/gemini-2.5-pro':'Gemini 2.5 Pro','google/gemini-2.5-flash':'Gemini 2.5 Flash','deepseek/deepseek-chat-v3-0324':'DeepSeek V3','meta-llama/llama-4-scout':'Llama 4 Scout'};
if(STATIC_LABELS[modelId]) return STATIC_LABELS[modelId];
return modelId.split('/').pop()||'Unknown';
}
@@ -1679,7 +1683,14 @@ function renderMessages(){
_assistantTurnBlocks(lastAssist).appendChild(usage);
}
}
scrollToBottom();
// Only force-scroll when not actively streaming — mid-stream re-renders
// (tool completion, session switch) must not override the user's scroll position.
// scrollIfPinned() respects _scrollPinned, so it's a no-op if user scrolled up.
if(S.activeStreamId){
scrollIfPinned();
} else {
scrollToBottom();
}
// Apply syntax highlighting after DOM is built
requestAnimationFrame(()=>{highlightCode();addCopyButtons();renderMermaidBlocks();renderKatexBlocks();});
// Refresh todo panel if it's currently open
@@ -2051,7 +2062,7 @@ function appendThinking(text=''){
}
row.className=(text&&String(text).trim())?'assistant-segment thinking-card-row':'assistant-segment';
row.innerHTML=_thinkingMarkup(text);
scrollToBottom();
scrollIfPinned();
}
function updateThinking(text=''){appendThinking(text);}
function removeThinking(){