Skip to content

fix: streaming Activity loss during scrolling - #3665

Closed
mysoul12138 wants to merge 10 commits into
nesquena:masterfrom
mysoul12138:fix/streaming-activity-loss
Closed

fix: streaming Activity loss during scrolling#3665
mysoul12138 wants to merge 10 commits into
nesquena:masterfrom
mysoul12138:fix/streaming-activity-loss

Conversation

@mysoul12138

Copy link
Copy Markdown
Contributor

Problem

During active streaming, loading earlier messages (scroll, button, or jump-to-start) causes:

  • Activity panel: all tool calls disappear (entire session)
  • Chat area: current streaming turn messages disappear

Root cause: _syncToolCallsForLoadedMessages clears S.toolCalls when messages have tool metadata, and S.busy=true blocks the renderMessages fallback rebuild. Additionally, _session_message_merge_key didn't include tool_calls, causing different tool-call messages to be incorrectly deduplicated.

Changes

Server (api/models.py) — data layer

  • _session_message_dedup_key: include tool_calls in dedup key
  • _session_message_visible_key: include tool_calls in visible key
  • _matching_visible_duplicate: return full key tuple, not 2-tuple
  • _session_message_merge_key: include tool_calls in merge key (root cause)

Server (api/routes.py)

  • Always return session-level tool_calls (don't clear when messages have per-message tool metadata)

Client (static/sessions.js)

  • _syncToolCallsForLoadedMessages: skip during S.busy/S.activeStreamId
  • _ensureMessagesLoaded: expand render window after loading messages
  • refreshActiveSessionIfExternallyUpdated: 5s _streamJustFinished cooldown

Client (static/messages.js)

  • done handler: _streamJustFinished cooldown + render window expansion
  • _restoreSettledSession: render window expansion

Client (static/ui.js)

  • Align _messageRenderableMessageCount with _visWithIdx filter

@greptile-apps

greptile-apps Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes Activity panel and chat message loss during scroll operations while streaming by addressing multiple root causes: including tool_calls in the server-side dedup/merge/visible key functions so tool-calling messages are no longer collapsed, removing the client-side _syncToolCallsForLoadedMessages clear that fired during streaming, and guarding refreshActiveSessionIfExternallyUpdated with a 5-second post-stream cooldown.

  • Server (models.py / routes.py): tool_calls is now included in all three key functions and the session-level tool_calls list is always returned to the client.
  • Client (sessions.js / messages.js / ui.js): _syncToolCallsForLoadedMessages now returns immediately when S.busy or S.activeStreamId is set; render-window size is expanded at key handoff points; renderMessages now re-renders settled tool cards even during streaming if S.toolCalls is populated.

Confidence Score: 5/5

Safe to merge; all changes are tightly scoped to the streaming/scroll race and the server-side dedup logic, with targeted regression tests added for the key paths.

The server-side key changes are well-guarded by new tests covering same/different tool_call identity, end-to-end merge, and the tail-payload shape. The client-side guards close discrete, understood races rather than introducing new state. No data-loss or auth paths are touched.

No files require special attention beyond the pre-existing observations already noted in earlier review threads.

Important Files Changed

Filename Overview
api/models.py Adds tool_calls to merge/dedup/visible key functions and refines the sidecar-timestamp guard to preserve distinct tool-call messages; _matching_visible_duplicate updated to handle new 3-tuple keys.
api/routes.py Removes conditional that zeroed _session_tool_calls when messages have per-message tool metadata; session-level tool_calls are now always returned.
static/sessions.js Adds streaming guard to _syncToolCallsForLoadedMessages and INFLIGHT guard to _ensureMessagesLoaded; expands render window after loading; adds _streamJustFinished cooldown to external-refresh gate.
static/messages.js Adds _streamJustFinished cooldown and render-window expansion in the done handler and _restoreSettledSession; clears visible-message-row cache after session data arrives.
static/ui.js renderMessages tool-card block now fires whenever S.toolCalls is non-empty; _visWithIdxCacheSrc reference check prevents stale cache on same-length wholesale replacement; _messageRenderableMessageCount now excludes recovery-control messages.
tests/test_merge_key_tool_calls.py New regression test file covering merge-key, dedup-key, visible-key, and end-to-end merge behaviour for messages with distinct tool_calls.
tests/test_session_tail_payload.py Renamed and updated test verifies session-level tool_calls are always returned windowed rather than cleared when messages have per-message tool metadata.

Reviews (9): Last reviewed commit: "Merge branch 'master' into fix/streaming..." | Re-trigger Greptile

Comment thread static/sessions.js
Comment thread api/routes.py
mysoul12138 added a commit to mysoul12138/hermes-webui that referenced this pull request Jun 5, 2026
- test_session_tail_payload: update assertion to expect windowed
  session-level tool_calls (PR nesquena#3665 always returns them)
- test_smooth_text_fade: use full-file search for _scheduleRender
  assertions instead of brace-counting function_block parser (the
  function is deeply nested and template literals confuse the counter)
@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Pulled the branch and read the diff across api/models.py, api/routes.py, static/sessions.js, static/messages.js, static/ui.js against origin/master, plus the merge reconciliation body merge_session_messages_append_only (models.py:4081-4262) where the key functions are consumed. The diagnosis is sound and the four-part fix (server payload + merge key + busy-guard + render-window) addresses distinct facets of the same symptom. A few things worth confirming before merge.

Server payload change is correct

Dropping the _messages_include_tool_metadata branch in routes.py:5122 and always returning the windowed session-level tool_calls is the right call:

# Always include session-level tool_calls so the browser can merge
# them with per-message tool_calls ...
if _windowed_messages:
    _session_tool_calls = _tool_calls_for_message_window(...)

The renamed test_session_tail_payload.py test now asserts the windowed list is returned (assistant_msg_idx: 0 preserved) rather than [], which matches the new contract. Good.

The merge-key change is the root-cause fix — but it's untested

Adding _tc_key to _session_message_visible_key, _session_message_dedup_key, and _session_message_merge_key (models.py:3896-3946) is the deepest change here, because those keys drive the sidecar↔state.db reconciliation in merge_session_messages_append_only. The _matching_visible_duplicate tuple-shape refactor (models.py:4000-4023) is internally consistent — by_role, loose_by_key, and the skipped_state_visible_counts/sidecar_visible_counts dicts are all keyed by the full existing_key, and the len(visible_key) > 1 guards are safe.

My concern: none of the three changed key functions has a direct regression test in this PR. grep -rln "_session_message_visible_key" tests/ returns nothing. The new _tc_key uses json.dumps(_tc, sort_keys=True, default=str) — so the two stores must serialize tool_calls identically for the same logical message, or the visible_key diverges and a row that previously deduped will now be emitted twice. If state.db ever carries tool_calls: None while the sidecar carries [] (or vice versa), _tc_key is "" in both cases (the if _tc else "" guard handles falsy), so that's fine — but if one store has the populated list and the other has it stripped/normalized differently (key ordering is handled by sort_keys, but nested non-serializable values fall back to default=str repr, which can differ), the keys won't match. This is exactly the append-only-duplication failure mode that test_issue2592_partial_dedupe.py and test_session_lost_response_regression.py exist to guard.

Recommend adding a test alongside those that feeds merge_session_messages_append_only a sidecar row + a state.db row representing the same tool-calling assistant message (matching tool_calls) and asserts they collapse to one, plus a second case with different tool_calls asserting both survive. That locks the actual contract rather than the source shape.

Client guards look right

The renderMessages change at ui.js:7089

if(!S.busy || (S.toolCalls&&S.toolCalls.length)){

— correctly still excludes live thinking via :not([data-live-thinking="1"]) in the removal selector (ui.js:7093), so allowing DOM insertion during S.busy won't strip the in-progress thinking card. The _streamJustFinished 5s cooldown in messages.js:2081 + the matching guard in refreshActiveSessionIfExternallyUpdated (sessions.js:2921) is a reasonable way to stop the post-done external-refresh from clobbering S.toolCalls. One nit: _streamJustFinished is a window global with a bare 5s setTimeout — if two turns complete within 5s the second's timer resets fine, but if a reload races the timer there's no cleanup on navigation. Low risk given it fails safe (just permits a refresh slightly early).

test_smooth_text_fade change

Switching that assertion from function_block(...) to scanning the whole MESSAGES_JS because the brace-counter chokes on ${...} template literals is pragmatic, but it weakens the test — the strings are no longer scoped to _scheduleRender, so an identical string appearing elsewhere would mask a regression. Acceptable given the parser limitation, just noting the reduced specificity.

Overall this is a solid fix for a real, multi-layered bug. The main ask is server-side merge-key test coverage before merge, since that's the highest-blast-radius change and currently unverified.

 regression)

- _session_message_dedup_key: add json.dumps(tool_calls) to key tuple
- _session_message_visible_key: add json.dumps(tool_calls) to key tuple
- _matching_visible_duplicate: return full key tuple, not 2-tuple
- _session_message_merge_key: add tool_calls to key tuple (root cause)

Without tool_calls in the keys, assistant messages with different
tool_calls arrays are incorrectly deduplicated, losing 23+ tool calls
per session. PR nesquena#3346 introduced the dedup_key without tool_calls.
Server (api/models.py):
- _session_message_dedup_key: include tool_calls in dedup key
- _session_message_visible_key: include tool_calls in visible key
- _matching_visible_duplicate: return full key tuple, not 2-tuple
- _session_message_merge_key: include tool_calls in merge key

Server (api/routes.py):
- Always return session-level tool_calls (don't clear when messages
  have per-message tool metadata)

Client (static/sessions.js):
- _syncToolCallsForLoadedMessages: skip during S.busy/S.activeStreamId
- _ensureMessagesLoaded: expand render window after loading messages
- refreshActiveSessionIfExternallyUpdated: 5s _streamJustFinished cooldown

Client (static/messages.js):
- done handler: set _streamJustFinished cooldown + expand render window
- _restoreSettledSession: expand render window before render
Add _statusCard and _assistantMessageHasVisibleContent checks to match
the _visWithIdx filter, preventing render window undercount.
- test_session_tail_payload: update assertion to expect windowed
  session-level tool_calls (PR nesquena#3665 always returns them)
- test_smooth_text_fade: use full-file search for _scheduleRender
  assertions instead of brace-counting function_block parser (the
  function is deeply nested and template literals confuse the counter)
… tool card rendering

- jumpToSessionStart: skip _ensureAllMessagesLoaded and renderMessages
  during streaming to prevent losing live messages and Activity
- _ensureMessagesLoaded: skip _syncToolCalls when INFLIGHT exists
  (INFLIGHT restore path will overwrite S.toolCalls)
- renderMessages: allow tool card DOM insertion during streaming when
  S.toolCalls is already populated (not blocked by S.busy)
- done handler: clearVisibleMessageRowCache after S.messages replacement
Two timestamp-based skip paths in merge_session_messages_append_only
unconditionally skipped legacy messages with timestamp <= max_sidecar,
without checking whether the merge_key was already registered by the
sidecar.  Messages with different tool_calls (but identical empty
content and same-second timestamp) were silently dropped.

Fix: add 'key in seen_message_keys' guard to both watermark paths
(lines ~4225 and ~4258) so only true duplicates are skipped.

Add test_merge_key_tool_calls.py with 10 regression tests covering
merge_key, dedup_key, visible_key, and end-to-end merge behavior
with same/different tool_calls.
@mysoul12138
mysoul12138 force-pushed the fix/streaming-activity-loss branch from d4fe9a7 to 55026d3 Compare June 5, 2026 17:01
- test_merge_key_tool_calls: remove unused json/pytest imports (F401)
- test_issue3306: increase _ensure_messages_loaded_body window from
  2500 to 3000 chars — the INFLIGHT guard addition pushed
  _pendingCarryForwardSnapshot = null past the old boundary (2505)
…ssages

The second watermark path (legacy key, timestamp <= max_sidecar_timestamp)
previously used 'key in seen_message_keys' which was too narrow — it let
through state.db messages whose merge_key differed from the sidecar only
by timestamp (e.g. pending_user_message with 'now' timestamp vs state.db
with the original timestamp), causing duplicates in gateway_chat tests.

Restore unconditional skip for legacy messages WITHOUT tool_calls.
For messages WITH tool_calls, only skip if the sidecar has the same
content but identical tool_calls (same dedup_key); if tool_calls differ
(same content_key, different dedup_key), preserve the state.db message
to avoid collapsing distinct tool-call turns.
@mysoul12138
mysoul12138 force-pushed the fix/streaming-activity-loss branch from 9deb6f7 to e03d6ff Compare June 5, 2026 19:04
mysoul12138 added a commit to mysoul12138/hermes-webui that referenced this pull request Jun 5, 2026
…earing

Upstream v0.51.253-271 re-introduced the logic that clears session-level
tool_calls when returned messages carry per-message tool metadata.
This regresses PR nesquena#3665: the browser needs session-level tool_calls for
the Activity panel even when messages have tool_calls inline.
nesquena-hermes added a commit that referenced this pull request Jun 5, 2026
…ng turn on mid-stream scroll #3665) (#3686)

* fix(streaming): preserve Activity + streaming turn when loading earlier messages mid-stream (#3665, #3346)

Co-authored-by: mysoul12138 <839465496@qq.com>

* docs(changelog): v0.51.279 — Release IU (stage-p3h)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: mysoul12138 <839465496@qq.com>
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request Jun 6, 2026
…➔ 0.51.293) (#856)

This PR contains the following updates:

| Package | Update | Change |
|---|---|---|
| [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.277` → `0.51.293` |

---

### Release Notes

<details>
<summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary>

### [`v0.51.293`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051293--2026-06-06--Release-JI-stage-s5--thinking-card-no-longer-renders-twice)

[Compare Source](nesquena/hermes-webui@v0.51.292...v0.51.293)

##### Fixed

- **The "Thinking" card no longer renders twice on a settled turn.** For a turn that had both a tool call and reasoning (e.g. think → call a tool → answer), the thinking card could appear once inside the collapsed **Activity** group at the top of the turn and again as a stranded second card below the answer and the `Done in …` footer. The thinking-only inline render path (added in v0.51.258 for [#&#8203;3592](nesquena/hermes-webui#3592)) now only fires when the turn has no Activity group of its own, and when it does render inline it inserts the card **above** the answer body instead of after the footer. Thinking that echoes the visible answer on a trailing reasoning-only message is also de-duplicated against the whole turn's answer text now, not just the same message's body. Genuinely thinking-only turns still show their thinking inline (the [#&#8203;3592](nesquena/hermes-webui#3592) fix is preserved, not reverted). ([#&#8203;3709](nesquena/hermes-webui#3709); supersedes [#&#8203;3708](nesquena/hermes-webui#3708))

### [`v0.51.292`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051292--2026-06-06--Release-JH-stage-s4--compression-exhausted-turns-surface-as-errors-not-fake-completions)

[Compare Source](nesquena/hermes-webui@v0.51.291...v0.51.292)

##### Fixed

- **Context-compression-exhausted turns are no longer finalized as a falsely "completed" response.** When Hermes Agent exhausts context compression in a long tool-heavy turn, the streamed result can end on a tool result or an assistant `tool_calls` turn with no final assistant answer. WebUI previously rendered that as a settled, completed reply. It now classifies a persisted transcript that ends in a tool/tool-call/empty-assistant tail (or an internal `[CONTEXT COMPACTION — REFERENCE ONLY]` marker) — and `compression_exhausted`/`failed`/`partial` agent results — as a terminal failure and surfaces a clear error instead. The compression session-id migration and pre-compression snapshot now run **before** the terminal-failure path returns, so frontend/backend session state stays consistent when exhaustion fires after the agent rotates `session_id`. ([#&#8203;3316](nesquena/hermes-webui#3316), [@&#8203;franksong2702](https://github.com/franksong2702); fixes [#&#8203;3315](nesquena/hermes-webui#3315))

### [`v0.51.291`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051291--2026-06-06--Release-JG-stage-s2--preserve-live-turn-content-when-switching-away-mid-stream)

[Compare Source](nesquena/hermes-webui@v0.51.290...v0.51.291)

##### Fixed

- **Switching away from a streaming session no longer loses the in-progress thinking/tool content.** When you clicked to another chat while a session was streaming during a quiet window (mid tool-execution or silent reasoning, between content events) and then switched back, the live turn's tool cards and thinking could disappear permanently — only the elapsed-time clock survived — until the response finished and the transcript re-rendered from the server. Cause: the live-turn DOM snapshot was only captured on content/`tool_complete` SSE events, so the switch-away teardown could run with a stale-or-absent snapshot, and the switch-back fallback rebuilt an empty thinking card. `closeLiveStream()` now snapshots the live turn **before** tearing the stream down, so switching back restores the exact state shown at switch-away. ([#&#8203;3668](nesquena/hermes-webui#3668))

### [`v0.51.290`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051290--2026-06-06--Release-JF-stage-s1--profile-providermodel-now-respected-in-session-resolution)

[Compare Source](nesquena/hermes-webui@v0.51.289...v0.51.290)

##### Fixed

- **Profile-bound sessions now resolve their provider and model from the profile** instead of silently falling back to the global active provider. Previously, when a chat was started under a profile and the model string was not `@provider:`-qualified (and no explicit provider was sent), the backend used the catalog's global active provider — so a profile wired to one provider/key could silently run on a different one, causing **wrong credentials/billing** and **silent context truncation** (the global default model's advertised context window could differ from what the provider actually served, so the provider dropped the oldest messages and long chats "forgot" earlier content). Resolution is now authoritative from the profile across all four runtime entry points (chat start, streaming worker incl. background/btw runs, and both deferred `/api/session` display resolvers); stale models are still repaired under the profile provider — including the `openai-codex` profile + stale `openai/…` slash-model case — while native slash IDs on OpenRouter/custom providers are preserved and explicit `@provider:` qualifiers still win. ([#&#8203;3448](nesquena/hermes-webui#3448), [@&#8203;rodboev](https://github.com/rodboev); fixes [#&#8203;3405](nesquena/hermes-webui#3405))

### [`v0.51.289`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051289--2026-06-06--Release-JE-hotfix--sidebar-ReferenceError-3696--scope-undef-prevention-gate)

[Compare Source](nesquena/hermes-webui@v0.51.288...v0.51.289)

##### Fixed

- **Sidebar no longer crashes with `ReferenceError: _sessionAttentionState is not defined`.** The session-attention helper was declared *inside* `renderSessionListFromCache()` and relied on function hoisting, but the top-level `_sidebarRowHasVisibleMessages` (reached via `renderSessionListFromCache` → `_partitionSidebarSessionRows`) called it bare — and hoisting is scoped to the enclosing function, so every sidebar cache-render threw and the session list went blank. `_sessionAttentionState` is now a top-level function reachable by both call sites. Regressed in [#&#8203;3672](nesquena/hermes-webui#3672) (v0.51.269). ([#&#8203;3696](nesquena/hermes-webui#3696))
- **Stale-stream terminal events no longer risk a `ReferenceError: source is not defined`.** `_bailOutOfTerminalEventsFromStaleStream` (declared inside `attachLiveStream`) called `_closeSource(source)` against a `source` that was not in its lexical scope — it would have thrown on the late-finalizing-stream path when the user is back in an active session. `source` is now threaded as an explicit parameter. Found by the new scope gate below during review. ([#&#8203;3696](nesquena/hermes-webui#3696))

##### Internal

- **New static-JS scope/undefined-reference gate (`scripts/scope_undef_gate.py`).** Models the WebUI's classic-`<script>` shared global scope and runs ESLint `no-undef` per file, flagging a function that is defined only *nested* but called from a sibling/top-level scope — the brick class behind [#&#8203;3696](nesquena/hermes-webui#3696) that `node --check`, source-presence tests, and the existing `no-const-assign` runtime gate all miss. Wired into the CI `lint` job alongside the `no-const-assign`/`no-import-assign` runtime gate, with an in-suite test (`tests/test_static_js_scope_undef.py`) and a focused structural regression test (`tests/test_issue3696_session_attention_scope.py`). ([#&#8203;3696](nesquena/hermes-webui#3696))

### [`v0.51.288`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051288--2026-06-06--Release-JD-stage-r24--collapsible-approval-card)

[Compare Source](nesquena/hermes-webui@v0.51.287...v0.51.288)

##### Added

- **The tool-call approval card can be collapsed to a thin header strip.** A chevron toggle in the approval-card header shrinks the card to just its "Approval required" heading so the tool-call rationale and transcript scrolled above it stay readable; clicking again re-expands it. Includes full ARIA (`aria-expanded`/`aria-controls`/`aria-label`), an icon swap, and transcript reflow that preserves a near-bottom scroll position. State resets to expanded for each new approval, so a fresh approval is never hidden. ([#&#8203;3515](nesquena/hermes-webui#3515), [@&#8203;rodboev](https://github.com/rodboev); closes [#&#8203;3007](nesquena/hermes-webui#3007))

### [`v0.51.287`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051287--2026-06-06--Release-JC-stage-r22--WeCom-session-classification--worker-profile-picker-hiding)

[Compare Source](nesquena/hermes-webui@v0.51.286...v0.51.287)

##### Fixed

- **WeCom gateway sessions are now classified as messaging conversations.** Rows arriving with raw sources `wecom` / `wecom_callback` are normalized into the messaging category (alongside weixin/telegram/discord/slack/email) and given proper "WeCom" / "WeCom Callback" display names, so they group and surface correctly in the sidebar. ([#&#8203;3653](nesquena/hermes-webui#3653), [@&#8203;franksong2702](https://github.com/franksong2702))

##### Changed

- **Worker profiles are hidden from the chat profile picker.** Worker profiles (used for orchestrator/Kanban dispatch) are no longer offered as normal human chat targets in the picker, while still appearing in the profile management view with a "Hidden from chat" badge. The active profile is never hidden. ([#&#8203;3662](nesquena/hermes-webui#3662), [@&#8203;Chukwuebuka-20](https://github.com/Chukwuebuka-20))

### [`v0.51.286`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051286--2026-06-06--Release-JB-stage-r21--sidebar-tab-reordering)

[Compare Source](nesquena/hermes-webui@v0.51.285...v0.51.286)

##### Added

- **Drag-reorder for sidebar tabs.** In Settings → Appearance, the "Sidebar tabs" chips (Tasks, Kanban, Skills, Memory, Spaces, Profiles, Todos, Insights, Logs) can be dragged to reorder how they appear in the left rail and sidebar nav, persisted via a sanitized `tab_order` setting (collapses duplicates, rejects `chat`/`settings`, strips non-strings). Chat and Settings stay fixed. Reorder is pointer/desktop-based (consistent with the existing Kanban drag-and-drop); the chips remain tappable for show/hide on touch. ([#&#8203;3067](nesquena/hermes-webui#3067), [@&#8203;ai-ag2026](https://github.com/ai-ag2026))

### [`v0.51.285`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051285--2026-06-06--Release-JA-stage-r19--update-reload-server-identity-race-fix)

[Compare Source](nesquena/hermes-webui@v0.51.284...v0.51.285)

##### Fixed

- **Don't reload the page until the *replacement* server is actually up after an update.** The post-update reload previously compared raw `/health` uptime, which couldn't distinguish a still-running old process from the restarted one (it could reload against the old process or hang). The client now reads a stable `server_started_at` identity before the update POST and reloads only once `/health` reports a *different* identity (with a null-baseline fallback). Both the force-update and regular apply paths read and pass the baseline. ([#&#8203;3654](nesquena/hermes-webui#3654), [@&#8203;franksong2702](https://github.com/franksong2702); [#&#8203;874](nesquena/hermes-webui#874))

### [`v0.51.284`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051284--2026-06-05--Release-IZ-stage-w4--sidebar-status-labels--cron-sessions-toggle)

[Compare Source](nesquena/hermes-webui@v0.51.283...v0.51.284)

##### Added

- **Manual session status labels (Todo / In Progress / Done).** Tag any session from its row's ⋯ menu with a colored status badge (blue Todo / amber In Progress / green Done), stored per-session in localStorage. The badge renders inline on the sidebar row and uses theme variables so it adapts to light/dark and skins. ([#&#8203;3570](nesquena/hermes-webui#3570), [@&#8203;rodboev](https://github.com/rodboev))
- **"Show cron sessions" preference** (Settings → Preferences). Surfaces cron-job output as conversations in the sidebar. Off by default and gated under "Show non-WebUI sessions" — only active once non-WebUI sessions are enabled — with a note that high-frequency jobs can flood the sidebar. ([#&#8203;3514](nesquena/hermes-webui#3514), [@&#8203;rodboev](https://github.com/rodboev); closes [#&#8203;2841](nesquena/hermes-webui#2841))

### [`v0.51.283`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051283--2026-06-05--Release-IY-stage-w2--composer-queue-hint-during-auto-compaction)

[Compare Source](nesquena/hermes-webui@v0.51.282...v0.51.283)

##### Fixed

- **The composer now tells you a message will queue during auto-compaction instead of looking dead.** While automatic compression runs, the send button previously went `disabled` with only a "Waiting for compression to finish" tooltip. It now shows a `queue` action with the placeholder + tooltip "Type a message — it will queue and send after compression", so you can type and have it sent automatically when compaction completes. ([#&#8203;3512](nesquena/hermes-webui#3512), [@&#8203;rodboev](https://github.com/rodboev); closes [#&#8203;3079](nesquena/hermes-webui#3079))

### [`v0.51.282`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051282--2026-06-05--Release-IX-stage-3544--surface-memoryskill-saves-in-Activity-summary)

[Compare Source](nesquena/hermes-webui@v0.51.281...v0.51.282)

##### Added

- **The collapsed Activity summary now shows when the agent saved a memory or updated a skill** — e.g. "Activity: 2 tools, 1 memory saved, 1 skill updated" — so persistent-state changes are visible at a glance without expanding the group. Detection matches the real tool action vocabularies (`memory`: add/replace count as saves, `remove` excluded; `skill_manage`: create/patch/edit/write\_file count as updates, delete/remove\_file excluded), and only completed, non-errored calls are counted. The memory/skill counts are subtracted from the tool count so it reflects only non-memory/skill tools. Classification is stamped as durable `data-*` attributes so the suffix survives the live tool-call group's HTML snapshot/restore on session switch. Sessions with no memory/skill writes render the unchanged "Activity: N tools" label. ([#&#8203;3544](nesquena/hermes-webui#3544), [@&#8203;rodboev](https://github.com/rodboev); closes [#&#8203;3340](nesquena/hermes-webui#3340))

### [`v0.51.281`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051281--2026-06-05--Release-IW-stage-verdigris--Verdigris-emeraldbronze-skin)

[Compare Source](nesquena/hermes-webui@v0.51.280...v0.51.281)

##### Added

- **New "Verdigris" appearance skin** — a dark-only emerald/forest-green palette (`#&#8203;0F1714` background, `#&#8203;121D18` sidebar) with bronze-gold accents (`#C89A5A`), named for the green-bronze patina on aged copper. Selectable in Settings → Appearance and via `/theme verdigris`. Fully scoped under `:root.dark[data-skin="verdigris"]` (no bleed into the default appearance or other skins), with component-level accents for the new-chat button, scrollbar, tool cards, tree viewer, session badges/tags, diff blocks, MCP status, and image lightbox. ([#&#8203;3602](nesquena/hermes-webui#3602), [@&#8203;rodboev](https://github.com/rodboev); closes [#&#8203;3357](nesquena/hermes-webui#3357))

### [`v0.51.280`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051280--2026-06-05--Release-IV-stage-p3i--Windows-self-update-restart-fix)

[Compare Source](nesquena/hermes-webui@v0.51.279...v0.51.280)

##### Fixed

- **Self-update now restarts correctly on Windows.** `os.execv` does not replace the current process on Windows (it spawns a new one while the old keeps running), so the old process held port 8787 and the new process failed to bind ("address already in use"), surfacing as "Update failed" after the timeout. On Windows the restart now launches a detached new process (`subprocess.Popen` with `DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP`) and exits the old one immediately to release the port, plus a bounded bind-retry loop in `server_bind()` (up to 10s) to ride out the `SO_EXCLUSIVEADDRUSE` teardown window. POSIX behavior is unchanged (still `os.execv`). ([#&#8203;3647](nesquena/hermes-webui#3647), [@&#8203;jja881](https://github.com/jja881))

### [`v0.51.279`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051279--2026-06-05--Release-IU-stage-p3h--preserve-Activitystreaming-turn-on-mid-stream-scroll)

[Compare Source](nesquena/hermes-webui@v0.51.278...v0.51.279)

##### Fixed

- **Loading earlier messages during an active stream no longer wipes the Activity panel or the current streaming turn.** Two causes: (1) the message merge/dedup keys didn't include `tool_calls`, so assistant messages invoking *different* tools with identical empty content and same-second timestamps collapsed into one — dropping every state.db tool-call after the first the sidecar registered; (2) `_syncToolCallsForLoadedMessages` cleared `S.toolCalls` while `S.busy` blocked the `renderMessages` rebuild. `tool_calls` is now part of the merge/dedup/visible keys (with a preservation branch so distinct tool invocations within the sidecar timestamp window aren't skipped), and the frontend keeps the live tool-call/streaming state when paging in history. ([#&#8203;3665](nesquena/hermes-webui#3665), [@&#8203;mysoul12138](https://github.com/mysoul12138); fixes [#&#8203;3346](nesquena/hermes-webui#3346))

### [`v0.51.278`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051278--2026-06-05--Release-IT-stage-p3g--repair-inline-PDF-preview)

[Compare Source](nesquena/hermes-webui@v0.51.277...v0.51.278)

##### Fixed

- **Inline PDF preview in chat now renders again.** The PDF.js loader previously created a `<script>` with both `src` and `textContent` set (the latter is ignored when `src` is present), so PDF.js never initialized and the preview hung on the spinner before degrading to a download link. It now loads PDF.js via a blob module script that sets the worker source, passes `isEvalSupported:false` to harden the parser, and revokes the blob URL on load. CSP gains `blob:` in `script-src` and a scoped `worker-src blob: 'self' https://cdn.jsdelivr.net` to permit the worker. ([#&#8203;3652](nesquena/hermes-webui#3652), [@&#8203;xx77yy](https://github.com/xx77yy); closes [#&#8203;3649](nesquena/hermes-webui#3649))

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about these updates again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwMS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJyZW5vdmF0ZS9jb250YWluZXIiLCJ0eXBlL3BhdGNoIl19-->

Reviewed-on: https://git.erwanleboucher.dev/eleboucher/homelab/pulls/856
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
…ng turn on mid-stream scroll nesquena#3665) (nesquena#3686)

* fix(streaming): preserve Activity + streaming turn when loading earlier messages mid-stream (nesquena#3665, nesquena#3346)

Co-authored-by: mysoul12138 <839465496@qq.com>

* docs(changelog): v0.51.279 — Release IU (stage-p3h)

---------

Co-authored-by: nesquena-hermes <[email protected]>
Co-authored-by: mysoul12138 <839465496@qq.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants