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:
@@ -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>
|
||||
|
||||
@@ -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;}}
|
||||
|
||||
21
static/ui.js
21
static/ui.js
@@ -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(){
|
||||
|
||||
Reference in New Issue
Block a user