Skip to content

fix: show auto-compression elapsed time#2512

Merged
2 commits merged into
nesquena:masterfrom
dso2ng:fix/2477-auto-compression-elapsed-indicator
May 18, 2026
Merged

fix: show auto-compression elapsed time#2512
2 commits merged into
nesquena:masterfrom
dso2ng:fix/2477-auto-compression-elapsed-indicator

Conversation

@dso2ng
Copy link
Copy Markdown
Contributor

@dso2ng dso2ng commented May 18, 2026

Thinking Path

  • Auto-compression during long WebUI runs appears stuck and hides compressed-session handoff #2477 identifies that long WebUI auto-compression can look frozen between the compressing SSE event and the later compressed event.
  • The issue's maintainer guidance splits this into independently mergeable slices; this PR implements Slice A only: a persistent running indicator with elapsed time.
  • The existing automatic-compression card already provides the right calm UI surface, so this keeps that card and adds a small elapsed timer instead of introducing a new backend event contract or sidebar behavior.
  • The state layer changed here is browser transient compression UI (window._compressionUi plus the live compression card DOM), not durable transcript history or model context.

What Changed

  • Stamp the automatic-compression running state with startedAt when the compressing SSE event arrives.
  • Render running automatic-compression cards as:
    • preview/header: Auto-compressing context... · 00:23
    • expanded body: Elapsed: 00:23
  • Once the 5-minute Slice A cap is reached, show 5+ min instead of freezing at 05:00.
  • Start a lightweight 1s timer while the running card is visible.
  • Update the existing card's preview/detail text in place; the timer does not call renderMessages() each tick, so it avoids disrupting live stream DOM.
  • Stop the timer when compression completes/clears, when the session no longer has a matching compression state, or after the 5-minute Slice A cap.
  • Preserve the existing live-card path by carrying data-compression-started-at / data-compression-message on anchored live compression cards.
  • Add regression coverage in tests/test_auto_compression_card.py.
  • Add an Unreleased changelog entry.

Why It Matters

Long compression can take minutes. Before this PR, the user saw the same generic running card the entire time, which made the browser feel stalled. The elapsed label gives a visible heartbeat while preserving the existing quiet compression-card design.

This intentionally does not implement #2477 Slice B/C:

  • no compressed-session handoff link,
  • no provider fallback/warning contract changes,
  • no backend SSE payload change,
  • no sidebar/session-list behavior change.

Visual Evidence

Before/after auto-compression elapsed indicator

Verification

Targeted local verification on the updated head:

node --check static/ui.js
node --check static/messages.js
python -m pytest \
  tests/test_auto_compression_card.py \
  tests/test_compression_snapshot_runtime_clear.py \
  tests/test_session_lineage_collapse.py \
  tests/test_streaming_session_sidebar.py \
  tests/test_issue734_message_windowing.py \
  tests/test_issue1617_tps_message_header.py \
  -q

Result:

74 passed

Hygiene:

git diff --check -> clean
non_ascii_added_lines=1

The one intentional non-ASCII added line is the existing public UI separator · in the elapsed preview.

Full local suite was also run on the updated head:

python -m pytest tests/ -q

Result:

5934 passed, 6 skipped, 3 xpassed, 8 subtests passed, 4 failed

The 4 failures are existing base failures in tests/test_issue1527_lmstudio_base_url_classification.py about prefixed model ids. I confirmed the same file fails the same way on clean origin/master@e6be01c4:

python -m pytest tests/test_issue1527_lmstudio_base_url_classification.py -q
# 4 failed, 1 passed

Risks / Follow-ups

  • Page reloads during an in-flight compression are not resume-on-reload. The timer state is browser-transient; after a reload the UI waits for any new compressing SSE state rather than reconstructing elapsed time from server history.
  • The timer is intentionally presentation-only and capped at 5 minutes per the Slice A guidance; it does not infer provider progress or compression phases.
  • If fix: clear fallback streaming warnings #2505 lands first, this may need a small rebase because both PRs touch tests/test_auto_compression_card.py, but the implementation layers are separate.
  • Slice B/C from Auto-compression during long WebUI runs appears stuck and hides compressed-session handoff #2477 remain open follow-ups for compressed-session handoff surfacing and fallback transparency.

Model Used

AI-assisted with Hermes Agent using OpenAI Codex provider, model gpt-5.5, with terminal/file/browser tooling for implementation, tests, and visual evidence capture.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Summary

Reading static/ui.js:1970-2010, 4905-4935, 5050-5070 on this branch against origin/master, the diff cleanly extends the existing auto-compression card with a 1-second-tick elapsed timer, capped at 5 minutes. It addresses the real complaint that an auto-compress card with no time signal looks frozen when the summarizer LLM call takes longer than expected. The implementation reuses the existing _formatActiveElapsedTimer helper, stamps startedAt on first SSE event, and persists the start time on the DOM so the live card stays in sync after renderMessages(). Solid shape; a few details worth tightening.

Code reference

The start-time stamping inside setCompressionUi:

const nextState={...state};
if(nextState.automatic&&nextState.phase==='running'&&!_compressionElapsedStartedAt(nextState)){
  nextState.startedAt=Date.now()/1000;
}
window._compressionUi=nextState;
if(nextState.sessionId) _setCompressionSessionLock(nextState.sessionId);
if(nextState.automatic&&nextState.phase==='running') _startCompressionElapsedTimer();
else _clearCompressionElapsedTimer();

The DOM-anchored fallback used after renderMessages() strips window._compressionUi:

function _compressionLiveCardState(){
  const node=_compressionLiveCardNode();
  const started=Number(node&&node.getAttribute('data-compression-started-at'));
  if(!node||!S.session||!Number.isFinite(started)||started<=0)return null;
  return {sessionId:S.session.session_id,phase:'running',automatic:true,
          message:node.getAttribute('data-compression-message')||'Auto-compressing context...',
          startedAt:started};
}

The cap at _COMPRESSION_ELAPSED_MAX_SECONDS=5*60 and the _compressionElapsedExpired check stop the timer from ticking forever if the compressed SSE event never arrives.

Diagnosis / Recommendation

Two correctness things and one small UX thing:

  1. Page-reload behavior. appendLiveCompressionCard stamps the start time onto the DOM node, but a full page reload rebuilds the DOM from renderMessages() reading server-side history — the live card hasn't been persisted yet (the compression card is rendered transiently from window._compressionUi, not from s.messages). So if a user reloads mid-compression, the elapsed counter resets to wherever the new SSE state arrives. That's probably acceptable, but worth noting in the PR description so reviewers don't expect resume-on-reload.
  2. 5-minute cap is the right ceiling but inaccurate framing. Once the counter hits 5:00, the timer clears via _clearCompressionElapsedTimer() but the card still displays 05:00 as the frozen value. If a real compression hangs past 5 minutes, the user sees a non-ticking "Elapsed: 05:00" and may think the elapsed display itself is broken. Consider switching the suffix at the cap from a frozen number to a textual fallback like Elapsed: 5+ min, or kicking off an "auto-compression appears stuck" inline notice. Related to issue Auto-compression during long WebUI runs appears stuck and hides compressed-session handoff #2477, which is already open.
  3. Detail vs preview duplication. _autoCompressionDetailText produces "${detail}\nElapsed: ${elapsedLabel}" (two lines), while _autoCompressionPreviewText produces "${detail} · ${elapsedLabel}" (single line). The preview is what shows in the card header; the detail is in the <pre> body. Functionally fine, but slightly redundant when a user expands the card — they see "Auto-compressing context... · 00:42" up top and "Auto-compressing context...\nElapsed: 00:42" below. Either drop the elapsed from the preview (header), or drop the redundant detail line from the body and only show Elapsed: ... there.

Test plan

The three new test cases in tests/test_auto_compression_card.py pin: the SSE handler stamps startedAt before calling setCompressionUi; the cards render _compressionElapsedLabel; and the live-card path persists data-compression-started-at + data-compression-message for DOM-anchored timer refresh. That covers the structural contracts. Worth adding one more: a state with startedAt set 6 minutes in the past should produce the capped label (05:00) and not call _startCompressionElapsedTimer on render.

Manual verification: trigger an auto-compression (paste a large message that exceeds the model's context, send a turn) and confirm the card shows a ticking Elapsed: MM:SS that stops at 5 minutes if the compression never completes.

CHANGELOG entry covers it. Ready to land once #2 (cap framing) is reconciled with the spirit of issue #2477.

@dso2ng
Copy link
Copy Markdown
Contributor Author

dso2ng commented May 18, 2026

Thanks for the detailed pass — I pushed d72b3382 to address the concrete items:

  • Page reload behavior: updated the PR description to explicitly call out that this is browser-transient state, not resume-on-reload state reconstructed from server history.
  • 5-minute cap framing: changed the capped label from a frozen 05:00 to 5+ min, so if compression exceeds the Slice A cap the UI no longer looks like a broken stopped timer.
  • Preview/detail duplication: kept the elapsed heartbeat in the card header/preview and changed the expanded body to show only Elapsed: ... for running cards.
  • Regression coverage: added tests for the non-frozen capped label and for avoiding duplicate message text in the expanded body.
  • Visual evidence: updated the PR body screenshot to show the new detail-body shape plus the 5+ min cap example.

Targeted local verification on the updated head:

node --check static/ui.js
node --check static/messages.js
python -m pytest \
  tests/test_auto_compression_card.py \
  tests/test_compression_snapshot_runtime_clear.py \
  tests/test_session_lineage_collapse.py \
  tests/test_streaming_session_sidebar.py \
  tests/test_issue734_message_windowing.py \
  tests/test_issue1617_tps_message_header.py \
  -q
# 74 passed

git diff --check -> clean
non_ascii_added_lines=1  # intentional public UI separator: ·

Full local suite on the updated head still has only the existing base-failure subset:

python -m pytest tests/ -q
# 5934 passed, 6 skipped, 3 xpassed, 8 subtests passed, 4 failed

The 4 failures are the same tests/test_issue1527_lmstudio_base_url_classification.py prefixed-model-id failures previously reproduced on clean origin/master@e6be01c4.

@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in 4589dbe May 18, 2026
Charanis pushed a commit to Charanis/hermes-webui-beyond that referenced this pull request May 18, 2026
# Conflicts:
#	CHANGELOG.md
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request May 18, 2026
… 0.51.90) (#556)

This PR contains the following updates:

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

---

### Release Notes

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

### [`v0.51.90`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05190--2026-05-18--Release-BN-stage-383--10-PR-full-sweep-batch--empty-gateway-messaging-history-fix--previous-messaging-sessions-setting--Kanban-board-switcher-layout--UIUX-demo-theme-controls--Slice-3c-queuegoal-RFC-gate--keyless-custom-endpoints--custom-provider-remote-model-catalog-parity--auto-compression-elapsed-timer--new-conversation-cold-start-guard--Kanban-drag-drop-detail-open-fix)

[Compare Source](nesquena/hermes-webui@v0.51.89...v0.51.90)

##### Fixed

- **PR [#&#8203;2286](nesquena/hermes-webui#2286 by [@&#8203;junjunjunbong](https://github.com/junjunjunbong) (refs [#&#8203;2275](nesquena/hermes-webui#2275)) — Narrow messaging stale-session filtering to active gateway sessions that are visible in the current sidebar candidate set. Older Discord/messaging history is now preserved when the gateway advertises a fresh zero-message session that hasn't yet entered the visible projection, instead of being hidden as stale. Adds a regression test for an empty active Discord gateway row preserving prior history.
- **PR [#&#8203;2459](nesquena/hermes-webui#2459 by [@&#8203;franksong2702](https://github.com/franksong2702) (closes [#&#8203;2458](nesquena/hermes-webui#2458)) — Fix the Kanban board switcher menu when a board's icon slot carries a long text label (e.g. `layout-kanban`). The icon column changed from a fixed `18px` slot to a bounded flex cell with `min-width:18px;max-width:7.5rem`, with overflow ellipsis on the icon itself so long labels render fully when space allows and truncate cleanly when not. Title and count columns keep stable spacing. Adds before/after screenshots and a CSS contract regression in `tests/test_kanban_ui_static.py`.
- **PR [#&#8203;2522](nesquena/hermes-webui#2522 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (refs [#&#8203;2271](nesquena/hermes-webui#2271)) — Treat named custom OpenAI-compatible endpoints with a configured `base_url` as key-optional at WebUI agent startup. Local keyless servers (llama-server / vLLM-style LAN deployments) no longer fail early with a synthetic `CUSTOM:<slug>_API_KEY` env-var prompt before the request reaches the endpoint; instead the OpenAI-compatible client initialises with a harmless placeholder key and real configured keys are still preferred when present. Refactors the three near-identical custom-provider rebuild blocks (initial agent setup + two retry/healing paths) through the existing `resolve_custom_provider_connection` helper.
- **PR [#&#8203;2515](nesquena/hermes-webui#2515 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (closes [#&#8203;2513](nesquena/hermes-webui#2513)) — Keep named custom-provider model pickers populated from each configured endpoint's live `/models` catalog even when `custom_providers[].model` is present. The singular `model` field now acts as a sticky/fallback entry appended *after* the remote catalog rather than collapsing the picker to just the configured model and hiding sibling named custom providers. Extracts reusable OpenAI-compatible `/models` parsing/fetching helpers and threads them through both the active-base-url and per-named-provider paths.
- **PR [#&#8203;2512](nesquena/hermes-webui#2512 by [@&#8203;dso2ng](https://github.com/dso2ng) (refs [#&#8203;2477](nesquena/hermes-webui#2477), Slice A) — Show an elapsed timer on the running automatic-compression card so long WebUI context-compression pauses no longer look frozen while the browser waits for the `compressed` event. Stamps `startedAt` on the `compressing` SSE event, ticks once per second, and switches to a `5+ min` cap label past the Slice A bound so the UI never frame-freezes at `05:00`. Browser-transient state only — no SSE contract change and no server-side resume reconstruction.
- **PR [#&#8203;2528](nesquena/hermes-webui#2528 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (closes [#&#8203;2518](nesquena/hermes-webui#2518)) — Guard New Conversation creation while a previous `/api/session/new` request is still in flight, so cold model/provider catalog resolution gives immediate pending feedback and rapid repeated clicks reuse the same create request instead of enqueueing duplicate blank sessions. Coalesces concurrent `newSession()` calls behind a single in-flight promise, disables the sidebar button with `aria-busy="true"`, and shows a localized `Creating new conversation…` composer status.
- **PR [#&#8203;2530](nesquena/hermes-webui#2530 by [@&#8203;franksong2702](https://github.com/franksong2702) (refs [#&#8203;2529](nesquena/hermes-webui#2529)) — Keep Kanban drag/drop status updates from also opening the task detail pane. Two failure paths were both producing detail-pane opens after drag/drop: the browser's trailing synthetic click after `drop`, and the generic task-update helper opening detail on every PATCH. The fix adds a time-windowed `_kanbanSuppressCardClickUntil` set on `ondragstart`/`ondragend`/`ondrop` and routes drag/drop status changes through a board-only update path. Explicit card click and keyboard activation remain unchanged.

##### Added

- **PR [#&#8203;2294](nesquena/hermes-webui#2294 by [@&#8203;junjunjunbong](https://github.com/junjunjunbong) — Add a `show_previous_messaging_sessions` setting so users can opt back into seeing previous messaging sessions that were replaced by `session_reset` or auto-compression. The preference is wired through boot, settings persistence, and the sidebar projection. Also adds a separate "Hide from list" action for imported messaging/CLI sessions that hides individual rows from the sidebar without deleting source history.

##### Documentation

- **PR [#&#8203;2511](nesquena/hermes-webui#2511 by [@&#8203;franksong2702](https://github.com/franksong2702) (refs [#&#8203;2502](nesquena/hermes-webui#2502) / [#&#8203;2503](nesquena/hermes-webui#2503)) — Update the `docs/ui-ux/` demo appearance controls to initialize as `class="dark" data-skin="slate"` instead of the deprecated `data-theme`-only buttons and legacy theme names. Brings the demo pages in line with the live Theme + Skin contract referenced from the new `docs/CONTRACTS.md` so contributors following the contract-index path don't land on stale demos.
- **PR [#&#8203;2509](nesquena/hermes-webui#2509 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (refs [#&#8203;1925](nesquena/hermes-webui#1925)) — Advance the runtime-adapter RFC after the Slice 3b approval/clarify seam shipped in v0.51.89. The RFC now marks Slice 3b as shipped and defines the next Slice 3c queue/continue + goal control gate: route those controls through `RuntimeAdapter.queue_message(...)` / `update_goal(...)` only after pinning stable response contracts, bounded unavailable-control behavior, replayable lifecycle/status evidence, ordering/idempotency expectations, and explicit non-goals for runner/sidecar ownership or a WebUI-owned queue/goal scheduler. Docs + adapter-seam regression test only — no runtime/control routing changes in this PR.

</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/556
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