mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-16 20:50:18 +00:00
Release v0.51.258 — Release HZ (stage-r6) (#3611)
## Release v0.51.258 — Release HZ (stage-r6) Fresh-arrival low-risk pair (both @rodboev, landed in the last sweep window). ### Fixed | Issue | Fix | |-------|-----| | #3597 | The "update available" banner now shows from **any panel** (Settings → System "Check now", etc.), not just the Chat view — it was positioned inside the chat surface so it only rendered there. | | #3592 | Under Simplified Tool Calling, an assistant turn with **thinking but no tool calls** now renders that thinking inline on settlement instead of burying it in an empty collapsed activity group. | ### Review fix absorbed (Codex) #3592's inline-render `continue` skipped the activity-group creation that carried the turn's `data-turn-duration`, but the footer still suppressed the "Done in …" duration for any `assistantThinking` turn → thinking-only turns silently lost their duration display. Fixed: footer duration is now suppressed **only** for turns that actually build an activity group (`toolCallAssistantIdxs.has(mi)`), so thinking-only inline turns keep "Done in …". + regression test. ### Gate - Full pytest suite: **7631 passed, 0 failed** - ESLint: CLEAN · browser-smoke: CLEAN - Codex (regression): SHIP-ONLY-WITH-FIXES (duration-drop) → fixed → **SAFE TO SHIP** ### Sweep note #3603 + #3604 (sidebar CLI-session classification, same author/area) were **not** included — they assert contradictory models for a sidecar-less `source='cli'` recovery row; flagged on both PRs for the author to reconcile. Co-authored-by: rodboev <rodboev@users.noreply.github.com>
This commit is contained in:
@@ -3,6 +3,12 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.51.258] — 2026-06-04 — Release HZ (stage-r6 — update banner from any panel + inline thinking on settlement)
|
||||
|
||||
### Fixed
|
||||
- The "update available" banner now appears from **any panel** (Settings → System "Check now", etc.), not just the Chat view — the banner element was positioned so it only rendered visibly on the Chat surface. (#3597, @rodboev)
|
||||
- Under Simplified Tool Calling, an assistant turn that produced **thinking but no tool calls** now renders that thinking inline on settlement instead of burying it in an empty collapsed activity group. (#3592, @rodboev)
|
||||
|
||||
## [v0.51.257] — 2026-06-04 — Release HY (stage-r5 — provider refresh route + SessionDB self-heal)
|
||||
|
||||
### Fixed
|
||||
|
||||
+16
-16
@@ -371,6 +371,22 @@
|
||||
<div class="resize-handle" id="sidebarResize"></div>
|
||||
</aside>
|
||||
<main class="main">
|
||||
<div class="update-banner" id="updateBanner">
|
||||
<div style="display:flex;flex-direction:column;flex:1;min-width:0">
|
||||
<span id="updateMsg"></span>
|
||||
<div id="updateWhatsNewLinks" style="display:none;font-size:11px;margin-left:8px;white-space:nowrap"></div>
|
||||
<div id="updateSummaryPanel" style="display:none;font-size:12px;line-height:1.45;margin-top:6px;padding:8px 10px;border:1px solid var(--border2);border-radius:8px;background:rgba(255,255,255,.04);max-width:720px;white-space:normal">
|
||||
<div id="updateSummaryText"></div>
|
||||
<div id="updateSummaryDiffLinks" style="display:none;font-size:11px;margin-top:8px"></div>
|
||||
</div>
|
||||
<div id="updateError" style="display:none;font-size:12px;color:var(--error,#e05);margin-top:4px;word-break:break-word"></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-shrink:0;flex-wrap:wrap">
|
||||
<button class="update-btn" onclick="dismissUpdate()">Later</button>
|
||||
<button class="update-btn update-primary" id="btnApplyUpdate" onclick="applyUpdates()">Update Now</button>
|
||||
<button class="update-btn" id="btnForceUpdate" style="display:none;background:var(--error,#e05);color:#fff;border-color:var(--error,#e05)" onclick="forceUpdate(this)">Force update</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mainChat" class="main-view">
|
||||
<div class="messages-shell">
|
||||
<button id="jumpToSessionStartBtn" class="session-jump-btn session-jump-btn--start" aria-label="Jump to beginning of session" data-i18n-aria-label="session_jump_start_label" data-i18n-title="session_jump_start_label" onclick="jumpToSessionStart()" style="display:none"><span aria-hidden="true">↑</span><span data-i18n="session_jump_start">Start</span></button>
|
||||
@@ -407,22 +423,6 @@
|
||||
<div id="liveToolCards" style="display:none;max-width:800px;margin:0 auto;width:100%;padding:0 24px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="update-banner" id="updateBanner">
|
||||
<div style="display:flex;flex-direction:column;flex:1;min-width:0">
|
||||
<span id="updateMsg"></span>
|
||||
<div id="updateWhatsNewLinks" style="display:none;font-size:11px;margin-left:8px;white-space:nowrap"></div>
|
||||
<div id="updateSummaryPanel" style="display:none;font-size:12px;line-height:1.45;margin-top:6px;padding:8px 10px;border:1px solid var(--border2);border-radius:8px;background:rgba(255,255,255,.04);max-width:720px;white-space:normal">
|
||||
<div id="updateSummaryText"></div>
|
||||
<div id="updateSummaryDiffLinks" style="display:none;font-size:11px;margin-top:8px"></div>
|
||||
</div>
|
||||
<div id="updateError" style="display:none;font-size:12px;color:var(--error,#e05);margin-top:4px;word-break:break-word"></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-shrink:0;flex-wrap:wrap">
|
||||
<button class="update-btn" onclick="dismissUpdate()">Later</button>
|
||||
<button class="update-btn update-primary" id="btnApplyUpdate" onclick="applyUpdates()">Update Now</button>
|
||||
<button class="update-btn" id="btnForceUpdate" style="display:none;background:var(--error,#e05);color:#fff;border-color:var(--error,#e05)" onclick="forceUpdate(this)">Force update</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="reconnect-banner" id="reconnectBanner">
|
||||
<span id="reconnectMsg"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="vertical-align:-1px"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> A response may have been in progress when you last left. Reload messages?</span>
|
||||
<div style="display:flex;gap:8px;flex-shrink:0">
|
||||
|
||||
+2
-2
@@ -3305,12 +3305,12 @@ main.main > #mainPlugin{display:none;}
|
||||
.settings-section-title{font-size:18px;font-weight:600;letter-spacing:-.01em;color:var(--text);line-height:1.3;margin-bottom:4px;}
|
||||
.settings-section-meta{font-size:13px;color:var(--muted);line-height:1.55;}
|
||||
.settings-version-badge{display:inline-flex;align-items:center;padding:3px 8px;border-radius:999px;background:var(--surface);color:var(--muted);font-size:11px;font-weight:600;font-family:'SF Mono',ui-monospace,SFMono-Regular,Menlo,monospace;flex-shrink:0;align-self:flex-start;letter-spacing:.02em;}
|
||||
#checkUpdatesBlock{display:inline-flex;align-items:center;gap:6px;font-size:12px;flex-shrink:0;align-self:flex-start;}
|
||||
#checkUpdatesBlock{display:inline-flex;align-items:center;gap:0.5em;font-size:12px;flex-shrink:0;align-self:flex-start;flex-wrap:wrap;}
|
||||
#checkUpdatesBlock .btn-tiny{padding:4px 10px;border-radius:8px;border:1px solid var(--border);background:var(--surface);color:var(--text);font-size:11px;font-weight:500;cursor:pointer;display:inline-flex;align-items:center;gap:5px;transition:border-color .15s,color .15s;}
|
||||
#checkUpdatesBlock .btn-tiny:hover{border-color:var(--accent);color:var(--accent);}
|
||||
#checkUpdatesBlock .btn-tiny:disabled{opacity:.5;cursor:default;}
|
||||
#checkUpdatesBlock .btn-tiny .spinner-xs{width:12px;height:12px;border:2px solid var(--border);border-top-color:var(--text);border-radius:50%;animation:spin .6s linear infinite;display:none;}
|
||||
#checkUpdatesStatus{font-size:11px;font-weight:500;white-space:nowrap;}
|
||||
#checkUpdatesStatus{font-size:11px;font-weight:500;white-space:nowrap;flex-basis:100%;margin-top:0.25em;}
|
||||
|
||||
/* Each logical form row is a card surface. Stack with comfortable gap. */
|
||||
#mainSettings .settings-field{margin-bottom:12px;padding:16px;border:1px solid var(--border);border-radius:12px;background:var(--sidebar);}
|
||||
|
||||
+14
-4
@@ -7213,6 +7213,13 @@ function renderMessages(options){
|
||||
const activityIdxs=[...new Set([...Object.keys(byAssistant).map(k=>parseInt(k)), ...assistantThinking.keys()])].sort((a,b)=>a-b);
|
||||
for(const aIdx of activityIdxs){
|
||||
const cards=byAssistant[aIdx]||[];
|
||||
if(!cards.length&&assistantThinking.has(aIdx)){
|
||||
const anchorRow=assistantSegments.get(aIdx);
|
||||
if(anchorRow&&window._showThinking!==false){
|
||||
anchorRow.insertAdjacentHTML('beforeend',_thinkingCardHtml(assistantThinking.get(aIdx)));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let anchorRow=assistantSegments.get(aIdx)||null;
|
||||
if(!anchorRow&&assistantIdxs.length){
|
||||
if(aIdx<assistantIdxs[0]) continue;
|
||||
@@ -7294,10 +7301,13 @@ function renderMessages(options){
|
||||
const failoverText=_gatewayRoutingFailoverText(routing);
|
||||
const modelWarningText=_gatewayModelWarningText(routing);
|
||||
const hasTurnUsage=!!msg._turnUsage;
|
||||
const compactActivityForMessage=isSimplifiedToolCalling()&&(
|
||||
assistantThinking.has(mi)||
|
||||
toolCallAssistantIdxs.has(mi)
|
||||
);
|
||||
// The activity-group summary owns the "Done in …" duration ONLY when a
|
||||
// group is actually created. A tool-call turn always builds one. A
|
||||
// thinking-only turn under Simplified Tool Calling now renders thinking
|
||||
// inline (no group — see the `continue` at the activityIdxs loop, #3592),
|
||||
// so it must keep its footer duration; suppressing it there would silently
|
||||
// drop "Done in …" for thinking-only turns (#3592 review).
|
||||
const compactActivityForMessage=isSimplifiedToolCalling()&&toolCallAssistantIdxs.has(mi);
|
||||
const durationText=compactActivityForMessage?'':_formatTurnDuration(msg._turnDuration);
|
||||
if(!hasTurnUsage&&!durationText&&!gatewayText&&!failoverText&&!modelWarningText) continue;
|
||||
const seg=assistantSegments.get(mi);
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
"""#3592 -- Thinking-only messages must render inline, not hidden in a collapsed activity group.
|
||||
|
||||
Under Simplified Tool Calling mode, the settlement loop wraps ALL post-settlement
|
||||
assistant content via ensureActivityGroup({collapsed:true}). When an assistant
|
||||
message has thinking but no tool calls, the thinking trace vanished behind a
|
||||
collapsed dropdown. Fix: early-continue guard so thinking-only messages render
|
||||
inline via _thinkingCardHtml instead of being wrapped.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
UI_JS = (Path(__file__).resolve().parent.parent / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_thinking_card_html_function_exists():
|
||||
"""_thinkingCardHtml must be defined so the inline path can call it."""
|
||||
assert "function _thinkingCardHtml(" in UI_JS, (
|
||||
"_thinkingCardHtml function must exist in ui.js"
|
||||
)
|
||||
|
||||
|
||||
def test_settlement_loop_has_empty_cards_guard():
|
||||
"""The simplified-tool-calling settlement loop must check cards.length before
|
||||
calling ensureActivityGroup, so thinking-only messages skip the collapsed group."""
|
||||
assert "!cards.length&&assistantThinking.has(aIdx)" in UI_JS, (
|
||||
"Settlement loop must guard on empty cards + thinking presence before "
|
||||
"wrapping in a collapsed activity group"
|
||||
)
|
||||
|
||||
|
||||
def test_early_continue_present_in_settlement_loop():
|
||||
"""The guard path must contain a continue statement so the activity group
|
||||
path is skipped for thinking-only messages."""
|
||||
guard_pattern = re.compile(
|
||||
r"!cards\.length&&assistantThinking\.has\(aIdx\).*?continue",
|
||||
re.DOTALL,
|
||||
)
|
||||
assert guard_pattern.search(UI_JS), (
|
||||
"The early-continue guard for thinking-only messages must be present "
|
||||
"in the settlement loop"
|
||||
)
|
||||
|
||||
|
||||
def test_alternative_path_calls_thinking_card_html_inline():
|
||||
"""The guard branch must call _thinkingCardHtml directly so thinking renders
|
||||
inline rather than inside a collapsed activity group."""
|
||||
guard_block = re.search(
|
||||
r"!cards\.length&&assistantThinking\.has\(aIdx\)(.*?)continue",
|
||||
UI_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert guard_block, "Guard block not found"
|
||||
block_text = guard_block.group(1)
|
||||
assert "_thinkingCardHtml(" in block_text, (
|
||||
"The early-continue branch must call _thinkingCardHtml to render "
|
||||
"thinking inline"
|
||||
)
|
||||
|
||||
|
||||
def test_show_thinking_preference_respected():
|
||||
"""The inline thinking path must check _showThinking so the preference is
|
||||
honoured the same way as the non-simplified path."""
|
||||
guard_block = re.search(
|
||||
r"!cards\.length&&assistantThinking\.has\(aIdx\)(.*?)continue",
|
||||
UI_JS,
|
||||
re.DOTALL,
|
||||
)
|
||||
assert guard_block, "Guard block not found"
|
||||
block_text = guard_block.group(1)
|
||||
assert "_showThinking" in block_text, (
|
||||
"The early-continue branch must respect window._showThinking"
|
||||
)
|
||||
|
||||
|
||||
def test_messages_with_tool_calls_still_use_activity_group():
|
||||
"""Messages that have tool calls must still flow through ensureActivityGroup
|
||||
so the existing collapsed-group behaviour is preserved."""
|
||||
assert "ensureActivityGroup(" in UI_JS, (
|
||||
"ensureActivityGroup must still be called for messages with tool calls"
|
||||
)
|
||||
|
||||
|
||||
def test_thinking_only_turns_keep_footer_duration():
|
||||
"""#3592 review regression: a thinking-only turn now renders inline with NO
|
||||
activity group, so the footer "Done in …" duration must NOT be suppressed for
|
||||
it — suppression belongs only to turns that actually build an activity group
|
||||
(tool-call turns). The old condition suppressed on assistantThinking.has(mi)
|
||||
too, which silently dropped the duration for thinking-only turns once the
|
||||
inline-render `continue` skipped group creation."""
|
||||
m = re.search(r"const compactActivityForMessage=isSimplifiedToolCalling\(\)&&([^;]+);", UI_JS)
|
||||
assert m, "compactActivityForMessage suppression condition not found"
|
||||
cond = m.group(1)
|
||||
assert "toolCallAssistantIdxs.has(mi)" in cond, (
|
||||
"duration suppression must key on toolCallAssistantIdxs (group actually created)"
|
||||
)
|
||||
assert "assistantThinking.has(mi)" not in cond, (
|
||||
"thinking-only turns must NOT suppress the footer duration (no group carries it)"
|
||||
)
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Verify #updateBanner lives outside #mainChat so it is visible from any panel."""
|
||||
import pathlib
|
||||
|
||||
INDEX = pathlib.Path(__file__).resolve().parent.parent / "static" / "index.html"
|
||||
|
||||
|
||||
def test_update_banner_outside_main_chat():
|
||||
src = INDEX.read_text(encoding="utf-8")
|
||||
banner_pos = src.find('id="updateBanner"')
|
||||
main_chat_pos = src.find('id="mainChat"')
|
||||
assert banner_pos != -1, "#updateBanner not found in index.html"
|
||||
assert main_chat_pos != -1, "#mainChat not found in index.html"
|
||||
assert banner_pos < main_chat_pos, (
|
||||
"#updateBanner must appear before #mainChat in the DOM "
|
||||
"so it is not hidden when non-Chat panels are active"
|
||||
)
|
||||
|
||||
|
||||
def test_update_banner_inside_main_element():
|
||||
src = INDEX.read_text(encoding="utf-8")
|
||||
main_pos = src.find('<main class="main">')
|
||||
main_end = src.find('</main>')
|
||||
banner_pos = src.find('id="updateBanner"')
|
||||
assert main_pos != -1, "<main class='main'> not found"
|
||||
assert main_end != -1, "</main> not found"
|
||||
assert main_pos < banner_pos < main_end, (
|
||||
"#updateBanner must be inside <main class='main'>, "
|
||||
"not before it or after the closing </main>"
|
||||
)
|
||||
Reference in New Issue
Block a user