Skip to content

Much faster agent profile switch and app startup - #2633

Merged
5 commits merged into
nesquena:masterfrom
dobby-d-elf:fast-profile-switching
May 20, 2026
Merged

5 commits merged into
nesquena:masterfrom
dobby-d-elf:fast-profile-switching

Conversation

@dobby-d-elf

@dobby-d-elf dobby-d-elf commented May 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Performance-only improvements to make app loading and agent profile switching much faster
  • Apply selected agent profile defaults immediately, refresh the visible/current screen before clearing the switch animation, and schedule non-visible workspace/model metadata in the background after the current view settles.
  • Keep profile catalog refreshes profile-generation-aware so stale background responses cannot overwrite the selected agent profile model/provider.
  • Start first-load session list rendering before unrelated workspace/onboarding metadata settles, with a smooth row-level flip animation for first load and profile changes.

No new or changed functionality is intended. This is scoped to perceived performance and smoothness only.

Measurements

Review follow-up

  • Boot synthetic model options now carry data-provider, and /api/settings exposes the default model provider so early sends before model-catalog hydration keep the provider.
  • Existing exact default-model options also get provider metadata, avoiding fuzzy fallback from gpt-5.4 to gpt-5.4-mini while the catalog is still hydrating.
  • The model catalog starts loading in the background when the agent profile is switched. This is one of the slowest operations in switching a profile which means the model-picking-menu will not be available for the first 1-5 seconds after an agent profile is switched. If a user clicks model picker menu while it is still loading, it will open when available. Picker open is capped so it cannot appear hung behind a slow catalog refresh. Mentioning this as an edge case/limitation - most users will not encounter it but if someone quickly switches agent profile AND then tries to switch the default model via the model picker, the menu can take a few extra seconds.
  • Workspace tree loading now has an inline comment explaining why hidden panels are fire-and-forget while visible panels are awaited.
  • Normal UI profile switching closes the profile picker before selecting a profile; stale background catalog responses are ignored if a profile switch happened while they were in flight.

Tests

  • Automated and manual verification performed

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

Summary

Reading PR head e89ef6ce against origin/master end-to-end — boot.js (39 lines), panels.js (87), sessions.js (69), api/profiles.py (3), plus the test edits — this is a well-instrumented perf PR. The framing "no new functionality, only perceived perf" matches what I see in the diff. The measurements in the body (10s → 0.3s on default→pepper) are plausible given that the master path awaits Promise.all([populateModelDropdown(), loadWorkspaceList()]) before the switch animation clears, and both of those hit network on every switch.

The shape of the change is sound: apply profile defaults from /api/profile/switch response immediately, kick off catalog refreshes in the background guarded by a generation counter, and lazy-hydrate the model catalog only when the picker opens.

Code reference

The new lazy-hydration trigger at static/ui.js:1453:

if(typeof window._ensureModelDropdownReady==='function') window._ensureModelDropdownReady();
const ready=window._modelDropdownReady;
if(ready&&typeof ready.then==='function'){
  try{await ready;}catch(_){}

Paired with the boot factory at static/boot.js:1547-1552:

const _startBootModelDropdown=()=>{
  const ready=window._modelDropdownReady;
  if(ready&&typeof ready.then==='function') return ready;
  const next=_hydrateBootModelDropdown();
  window._modelDropdownReady=next;
  return next;
};

And the generation guard in switchToProfile at static/panels.js:4769:

const _switchGen = ++_profileSwitchGeneration;
...
if (_switchGen !== _profileSwitchGeneration) return;

This is the correct fix for the stale-promise hazard that opens up the moment you stop awaiting in-line.

Diagnosis

1. Backend payload addition. api/profiles.py:917,974 adds default_model_provider to the /api/profile/switch response. That's exactly the right minimal backend change — the client can now apply both model and provider without an extra round-trip to /api/models. The provider comes from model_cfg.get('provider') on the per-profile config.yaml, which is the same source as the agent's resolver, so the two should stay in sync.

2. Workspace duplicate-write elimination. The sessionInProgress branch in master makes two round-trips: await newSession(false) (which POSTs /api/session/new with inheritWs), then await api('/api/session/update', ...) to set the workspace again. The PR removes the second call because S._profileSwitchWorkspace is set at panels.js:4807 before newSession() runs, and newSession() already consumes that flag at sessions.js:460:

const switchWs=S._profileSwitchWorkspace;
S._profileSwitchWorkspace=null;
const inheritWs=switchWs||(S.session?S.session.workspace:null)||(S._profileDefaultWorkspace||null);

So one fewer POST per switch, with the same observable result. The updated test_sprint40_ui_polish.py correctly inverts the old assertion (assertNotIn('/api/session/update', block)).

3. FLIP animation correctness. _captureSessionListFlipPositions() at sessions.js:1926 is called BEFORE list.innerHTML='' (which clears the DOM) — verified at sessions.js:2876-2880. The captured Map<sid, top> is then used to compute delta after re-render. Standard FLIP technique, implemented correctly. prefers-reduced-motion is honored at sessions.js:1944 and style.css:746-749. Good.

4. Generation counter. _profileSwitchGeneration properly handles the case where a user clicks Profile A, then quickly clicks Profile B — the older promise's continuations all short-circuit via if (_switchGen !== _profileSwitchGeneration) return;. There are five such early-returns in switchToProfile, plus the catch and finally arms gate their visible effects on the same comparison. This is the right pattern.

Concerns

1. Boot-time synthetic option lacks data-provider. At static/boot.js:1428-1437:

const opt=document.createElement('option');
opt.value=s.default_model;
opt.textContent=typeof getModelLabel==='function'?getModelLabel(s.default_model):s.default_model;
opt.dataset.custom='1';
sel.querySelectorAll('option[data-custom]').forEach(o=>o.remove());
sel.appendChild(opt);

No opt.dataset.provider. If the first /api/chat/start POST happens before populateModelDropdown() resolves (because the user types and hits send within ~50ms of boot), the model will be sent without a provider, and the agent has to fall back to the legacy provider resolver. Compare with the equivalent code in switchToProfile at panels.js:4801-4805 which does set opt.dataset.provider. Suggest adding:

opt.dataset.provider = window._activeProvider || '';

right before sel.appendChild(opt) in the boot branch.

2. _refreshProfileSwitchBackground clobbers window._modelDropdownReady to null at panels.js:4502. If the picker is already open at the moment the user triggers a profile switch (e.g. they had it open to verify the current model), the dropdown's current _modelData reflects the old profile until the user closes and reopens. That might be fine — the dropdown re-queries the dropdown options via populateModelDropdown on next open via _ensureModelDropdownReady — but worth confirming the open-dropdown UX manually. The model chip on the topbar gets the new default immediately, so this is probably only a visible inconsistency in the (rare) open-dropdown-during-switch case.

3. The awaitWorkspaceLoad option on newSession() is a clean way to make the workspace panel render synchronously when visible and async when hidden. The implementation at sessions.js:521-523 returns the promise from loadDir('.') and conditionally awaits. Worth a brief comment near that gate explaining why the asymmetry exists, otherwise the next reader will assume it's a missing await. Something like:

// loadDir('.') is fire-and-forget when the workspace panel is closed —
// awaiting it would block the new-session flow for users who never look
// at the file tree. When the panel is visible we await so the file
// list renders synchronously with the session swap.

4. Test surface. The new tests are entirely source-text assertions (look for specific JS strings). That's fine for catching accidental reverts but won't catch a refactor that preserves the strings but breaks the behavior. The FLIP test in particular (test_profile_refresh_captures_row_positions) asserts getBoundingClientRect().top is present in the file — true even if you delete the call site and stash it in a comment somewhere. Not blocking, but a small puppeteer-style integration test that loads the page and asserts the session list animates on profile change would be more robust. Skip if puppeteer isn't already in CI.

5. Measurements need a baseline anchor. The body says "10s → 0.3s" but doesn't note install size. The 10s case is presumably consistent with #2628's diagnosis (get_cli_sessions scanning a large state.db) — if the user has show_cli_sessions=true and a 2.7GB state.db, this PR will help perceived switch speed but not the underlying API latency. Worth mentioning in the PR body that the 0.3s figure is on a moderate-size install and that show_cli_sessions=true users may still see longer waits due to #2628.

Recommendation

Approve with the four concerns above noted as follow-ups (none blocking). The generation guard pattern is exactly right for an "apply optimistically, refresh in background" perf path, the FLIP animation is implemented properly with reduced-motion honored, and the duplicate workspace-update elimination is a clean cleanup. Add opt.dataset.provider on the boot synthetic option (concern 1) before merge if convenient — that's the only one with a behavioral risk.

@dobby-d-elf dobby-d-elf changed the title Optimize profile switching and session list loading Much faster agent profile switch and app startup May 20, 2026
@dobby-d-elf
dobby-d-elf force-pushed the fast-profile-switching branch 3 times, most recently from 3c91e33 to fd7212b Compare May 20, 2026 15:33
@dobby-d-elf
dobby-d-elf marked this pull request as draft May 20, 2026 15:38
@dobby-d-elf

Copy link
Copy Markdown
Contributor Author

moving to draft while I iron out a few issues

@dobby-d-elf

Copy link
Copy Markdown
Contributor Author
  1. Boot-time synthetic model options now carry provider metadata via opt.dataset.provider = window._activeProvider || '', so an early /api/chat/start before catalog hydration still sends the provider instead of falling back to legacy resolution.
  2. For profile switching and the model picker, we now invalidate and immediately warm the model catalog in the background after a profile switch. The picker itself stays conservative: if the user clicks before the catalog finishes, it still waits rather than showing stale options.
  3. Added the loadDir('.') comment explaining why hidden workspace trees are fire-and-forget while visible workspace panels are awaited during session/profile swaps.
  4. Tightened the FLIP test coverage without adding Puppeteer/Playwright, since browser automation is not already in CI. The test now verifies capture-before-render and play-after-render ordering instead of only checking for broad source strings.
  5. Updated the PR body measurement caveat: the 0.3s figure is from a moderate-size install, and users with show_cli_sessions=true plus very large state.db files may still see longer waits due to the underlying bug(cli-sessions): show_cli_sessions can make /api/sessions hit the 30s WebUI timeout on large state.db #2628 API latency.

@dobby-d-elf
dobby-d-elf marked this pull request as ready for review May 20, 2026 16:13
@dobby-d-elf

Copy link
Copy Markdown
Contributor Author

@nesquena-hermes this is ready to review again. Tested and addressed review comments

@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in f4a7989 May 20, 2026
dobby-d-elf pushed a commit to dobby-d-elf/hermes-webui that referenced this pull request May 20, 2026
# Conflicts:
#	CHANGELOG.md
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request May 21, 2026
… 0.51.103) (#594)

This PR contains the following updates:

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

---

### Release Notes

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

### [`v0.51.103`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051103--2026-05-21--Release-CA-stage-396--1-PR-follow-on--Settings--Plugins-distinguishes-exclusiveprovider-activation)

[Compare Source](nesquena/hermes-webui@v0.51.102...v0.51.103)

##### Fixed

- **PR [#&#8203;2663](nesquena/hermes-webui#2663 by [@&#8203;Fail-Safe](https://github.com/Fail-Safe) (closes [#&#8203;2659](nesquena/hermes-webui#2659)) — Settings → Plugins panel now distinguishes exclusive plugins (memory providers, web backends, browser providers activated via `<category>.provider` config) from disabled-or-broken plugins. The `/api/plugins` payload gains `kind` + `activation` fields; cards render a new "Active (provider)" badge variant for exclusive activation instead of mislabeling these plugins as "Disabled / No registered lifecycle hooks". Purely additive — for users without an exclusive provider configured, the panel renders "Enabled" and "Disabled" badges exactly as before. The legacy `enabled` boolean is preserved on the payload for back-compat with older WebUI clients; new clients read `activation` first with a fallback. 3 new behavioral tests cover the exclusive, model-provider, and standalone code paths.

### [`v0.51.102`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051102--2026-05-21--Release-BZ-stage-395--1-PR-follow-on--capped-CLI-sidebar-candidate-window-now-keyed-on-last-activity-not-start-time)

[Compare Source](nesquena/hermes-webui@v0.51.101...v0.51.102)

##### Fixed

- **PR [#&#8203;2662](nesquena/hermes-webui#2662 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (closes [#&#8203;2656](nesquena/hermes-webui#2656)) — Capped CLI/agent sidebar scans now order the candidate CTE by `COALESCE(MAX(messages.timestamp), s.started_at)` instead of `s.started_at` alone. Long-lived CLI sessions that were resumed days later (old `started_at`, recent message activity) stay visible in the candidate window instead of falling outside the 8×limit oversample. Closes the regression I filed against v0.51.99's [#&#8203;2647](nesquena/hermes-webui#2647) sidebar candidate-window narrowing. New regression test creates an old session with a recent message timestamp and asserts it surfaces at the top.

### [`v0.51.101`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051101--2026-05-20--Release-BY-stage-394--2-PR-deep-review-batch--workspace-Git-backend--sidebar-tab-visibility-toggle)

[Compare Source](nesquena/hermes-webui@v0.51.100...v0.51.101)

##### Added

- **PR [#&#8203;2625](nesquena/hermes-webui#2625 by [@&#8203;stocky789](https://github.com/stocky789) — Add backend Git operations for the workspace panel. New `api/workspace_git.py` module exposes read-only ops (`/api/git/status`, `/api/git/branches`, `/api/git/diff`, `/api/git/commit-message[-selected]`) unconditionally and mutating ops (`stage`, `unstage`, `discard`, `commit`, `commit-selected`, `checkout`, `stash-checkout`, `pull`, `push`) only when `HERMES_WEBUI_WORKSPACE_GIT_DESTRUCTIVE=1` is set in the environment — default OFF so existing deployments are unaffected. All subprocess calls use `["git", *args]` with `shell=False`, all branch/ref names go through `git check-ref-format --branch` validation before flowing to `git switch -c`, and `subprocess.env` is scrubbed of `GIT_DIR`/`GIT_WORK_TREE`/`GIT_CONFIG_GLOBAL`/`GIT_CONFIG_SYSTEM`/`GIT_CONFIG_COUNT`/`GIT_CONFIG_PARAMETERS` plus the full `GIT_CONFIG_KEY_*`/`GIT_CONFIG_VALUE_*` namespace before every invocation. `GIT_INDEX_FILE` is intentionally preserved to drive selected-file commits through a private temporary index. Paths are bound to the workspace root via `safe_resolve_ws()` + `Path.relative_to()` enforcement (rejects `..` traversal and symlinked escapes); active-stream gate prevents mutations during a running agent turn. Documented in `docs/workspace-git.md` with the full trust model (hooks-as-RCE warning, default-allowed vs gated lists, env-scrub enumeration). Frontend UI ships in a follow-up PR.
- **PR [#&#8203;2636](nesquena/hermes-webui#2636 by [@&#8203;FrancescoFarinola](https://github.com/FrancescoFarinola) — Per-tab sidebar visibility toggle in Settings → Appearance. Power users can hide unused rail tabs (Tasks, Kanban, Skills, Memory, Spaces, Profiles, Todos, Insights, Logs) while keeping Chat and Settings always reachable. Settings is per-profile so each profile can have its own hidden-tabs preference; an inline `<script>` in `<head>` applies `nav-tab-hidden` from `localStorage` before first paint so toggled-off tabs don't flash visible on reload. Default off — no tabs are hidden out of the box; existing deployments are unaffected. Chips use `role="switch"` + `aria-checked` for clear screen-reader narration, and the container has `role="group"` + `aria-labelledby` pointing at its label. Backend validator strips `chat` and `settings` from `hidden_tabs` at save time as a belt-and-suspenders against tampered POSTs. Profile switch reconciliation: `_refreshProfileSwitchBackground` re-fetches `/api/settings` and re-applies `hidden_tabs` after a profile change so the new profile's preference takes effect immediately.

##### Maintainer additions during stage

- `_refreshProfileSwitchBackground` profile-switch reconciliation for [#&#8203;2636](nesquena/hermes-webui#2636) (Profile A's hidden-tabs no longer bleeds into Profile B until Settings is opened).
- `role="switch"` + `aria-checked` chip a11y for [#&#8203;2636](nesquena/hermes-webui#2636) (was `aria-pressed` — confusing polarity for users where chip-off looks like the off state).
- Server-side `hidden_tabs` validator strip of `chat`/`settings` for [#&#8203;2636](nesquena/hermes-webui#2636).
- CSS contrast fix for [#&#8203;2636](nesquena/hermes-webui#2636) — `color: #&#8203;1a1a1a` + `font-weight: 600` on filled chips (was `color: var(--bg-page)` which resolved to white in dark theme and was barely readable on the gold accent).
- 3 new regression tests for the [#&#8203;2636](nesquena/hermes-webui#2636) maintainer additions (profile-switch wiring, chat/settings server-side strip, a11y switch role).

##### UX approval

PR [#&#8203;2636](nesquena/hermes-webui#2636) went through the full multi-viewport screenshot gate (390 mobile, 1280 laptop, 1440 desktop, 1920 wide; both light and dark themes; default-on and 3-off mixed states; rail-effect proof showing hidden tabs collapse cleanly). Approved via Telegram for merge.

### [`v0.51.100`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051100--2026-05-20--Release-BX-stage-393--3-PR-deep-review-batch--lazy-journal-recovery-retry--faster-profile-switch--cross-tab-session-list-SSE-sync)

[Compare Source](nesquena/hermes-webui@v0.51.99...v0.51.100)

##### Fixed

- **PR [#&#8203;2615](nesquena/hermes-webui#2615 by [@&#8203;Isla-Liu](https://github.com/Isla-Liu) — Self-heal stalled interrupted-turn markers by lazily retrying run-journal recovery on subsequent sidebar/full-load reads. When `_apply_core_sync_or_error_marker` lands in the "interrupted, no journal yet" branch, the session now carries `_pending_journal_recovery` metadata so the next `get_session()` call re-runs `_append_journaled_partial_output(..., dedupe_existing=True)` if a fresh journal has materialized in the meantime. Bounded by three independent ceilings: `_JOURNAL_RETRY_MAX_ATTEMPTS = 12` (attempts on sealed-but-unrecoverable journals), `_JOURNAL_RETRY_GIVEUP_SECONDS = 24 * 3600` (wall-clock cap that fires even when the journal never materializes), and an explicit "no stream id → demote immediately" branch. Per-session `threading.Lock` (non-blocking acquire) guards concurrent `get_session()` callers; second caller no-ops to `False`. Closes a real WSL2/network-FS class of bug where the recovery marker stuck on a transiently-missing journal and never self-healed without a server restart.

##### Changed

- **PR [#&#8203;2633](nesquena/hermes-webui#2633 by [@&#8203;dobby-d-elf](https://github.com/dobby-d-elf) — Make agent profile switching and initial app boot dramatically faster (\~25× on the contributor's benchmark). Switch-time: applies the selected profile's defaults immediately (resolved via a per-profile `_profile_default_model_state(profile)` config read), refreshes the visible/current screen, schedules non-visible workspace/model metadata in the background after the current view settles. A `_profileSwitchGeneration` counter is checked at five `await` boundaries to abort stale switches when the user re-toggles mid-refresh. Boot-time: workspace and onboarding fetches run in parallel instead of serial; onboarding short-circuits with `Promise.resolve(false)` when `onboarding_completed` is true. Boot synthetic model options now carry `data-provider` metadata so a pre-catalog `/api/chat/start` doesn't fuzzy-fallback to the wrong provider. FLIP animation capture/play is index-pinned by a new positional test; `prefers-reduced-motion` is honored in both JS and CSS. Server-side `new_session()` resolves the requesting profile's `config.yaml` directly rather than the process-global, preventing cross-tagging when the URL omits `model`/`model_provider`.

##### Added

- **PR [#&#8203;2637](nesquena/hermes-webui#2637 by [@&#8203;dobby-d-elf](https://github.com/dobby-d-elf) — Push lightweight session-list invalidation events to connected browsers via a new in-process pub/sub bus (`api/session_events.py`) and SSE endpoint (`/api/sessions/events`). Triggered on every visible-session mutation: new (only after worktree creation or first-message reveal), delete, archive, move, pin, rename, duplicate, import, and cron-job completion. Per-subscriber `Queue(maxsize=1)` with latest-wins drain — under burst, browsers see the freshest payload, not a backlog. Disconnect cleanup runs in the handler's `finally:` block, scoped to the shared `_CLIENT_DISCONNECT_ERRORS` tuple so socket-failure modes beyond `BrokenPipeError` also trigger unsubscribe. Cron-scheduler integration uses a `_cron_profile_context_depth()` gate to avoid double-publish across manual vs scheduled cron run paths. **Follow-on issue filed:** the bus has no profile identity in its payload — a cron tick in profile A wakes the SSE handler for every browser regardless of which profile its session cookie selects, costing wasted `/api/sessions` round-trips on other-profile tabs (no data leak; the GET is profile-scoped). Filing tracker for a follow-up that snapshots the active profile at publish time.

##### Test infrastructure

- HTTP integration tests for `/api/sessions/events`: handshake (200 + `text/event-stream`), event delivery driven by a side-effect POST, rapid open/close burst that survives a subsequent `/api/sessions` GET, and a source-level guard that the handler's `except _CLIENT_DISCONNECT_ERRORS:` clause stays wired to the shared tuple. Added as stage-393 follow-up to Opus advisor's blocking note on PR [#&#8203;2637](nesquena/hermes-webui#2637) (the existing tests covered the in-process bus but never opened a real HTTP connection).

### [`v0.51.99`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05199--2026-05-20--Release-BW-stage-392--5-PR-batch--compact-tool-activity-grouping--CLI-sidebar-scan-cap--title-generation-API-key-forwarding--post-compression-replay-dedup--clarify-popup-stability)

[Compare Source](nesquena/hermes-webui@v0.51.98...v0.51.99)

##### Fixed

- **PR [#&#8203;2638](nesquena/hermes-webui#2638 by [@&#8203;dobby-d-elf](https://github.com/dobby-d-elf) — Keep compact tool activity grouped under a single Activity disclosure within an assistant turn. The compact renderer was splitting Thinking and interim tool activity into multiple visible fragments inside the same turn. Composes cleanly with v0.51.96's [#&#8203;2620](nesquena/hermes-webui#2620) (second-level dedup key) — the bundled `merge_session_messages_append_only()` change now only adds to `seen_message_keys` when the key is an authoritative `message_id`-prefixed key, preserving the v0.51.96 invariant that two distinct state-only same-second rows must remain visible.
- **PR [#&#8203;2647](nesquena/hermes-webui#2647 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (closes [#&#8203;2628](nesquena/hermes-webui#2628)) — Cap the CLI/agent session sidebar bridge to a recent-candidate window before joining message rows. Previously `show_cli_sessions` ran a `LEFT JOIN messages ... GROUP BY s.id` across the entire Hermes `state.db`, aggregating 100k+ message rows before applying the visible sidebar cap. Large installs paid that cost on every sidebar read. The capped path now selects an oversampled candidate set (8× the visible limit) ordered by `started_at DESC`, then runs the message aggregate inside that window. Uncapped callers (full scans, exports) are unchanged.
- **PR [#&#8203;2650](nesquena/hermes-webui#2650 by [@&#8203;starship-s](https://github.com/starship-s) — Forward the configured `auxiliary.title_generation.api_key` for config-derived title routes. Completes [#&#8203;2612](nesquena/hermes-webui#2612) / v0.51.96 — the prior PR routed provider/model/base\_url through but the API key was left to fall back to the chat client's key, which doesn't match the Hermes Agent task-config shape and silently failed for setups where the title model lives behind a different account. The new `caller_supplied_route` guard prevents leakage of the title-generation key to caller-supplied (active-agent fallback) routes.
- **PR [#&#8203;2651](nesquena/hermes-webui#2651 by [@&#8203;LumenYoung](https://github.com/LumenYoung) (refs [#&#8203;1217](nesquena/hermes-webui#1217)) — Dedupe replayed active-context tails before appending agent result deltas to the WebUI display transcript, and apply the same replay protection to persisted `context_messages`. Without this, post-compression continuation re-fed an already-present tail into the next model turn, inflating the model-facing context and bloating the visible transcript with duplicate assistant cards.
- **PR [#&#8203;2643](nesquena/hermes-webui#2643 by [@&#8203;arshkumarsingh](https://github.com/arshkumarsingh) (closes [#&#8203;2639](nesquena/hermes-webui#2639)) — Require a stable `clarify_id` and wait for the backend ack before hiding the WebUI "Clarification needed" popup. Three bugs conspired to cause stale clarifications to silently fail: (a) `_ClarifyEntry` had no unique identifier so the frontend couldn't reference a specific pending prompt; (b) the backend used FIFO resolution which silently dropped late/stale responses; (c) the frontend hid the popup before the POST returned, so users saw a successful submit while the agent fell back to its best-judgement timeout path. Server-side `clarify_id` is now generated in `_ClarifyEntry.__init__` (UUID-based), propagated through SSE/poll payloads, sent back by the browser, and matched via `resolve_clarify_by_id()`. The popup stays visible until the POST returns; a 409/`stale:true` response keeps the draft and shows a toast. The legacy `not bool(clarify_id)` quirk that always returned `ok:true` is gone.

### [`v0.51.98`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05198--2026-05-20--Release-BV-stage-391--1-PR-follow-on--customproviders-allowlist-priority-over-live-v1models)

[Compare Source](nesquena/hermes-webui@v0.51.97...v0.51.98)

##### Fixed

- **PR [#&#8203;2640](nesquena/hermes-webui#2640 by [@&#8203;colin-chang](https://github.com/colin-chang) — When a `custom_providers` entry in `config.yaml` declares a curated `models:` allowlist (e.g. a ZenMux or other aggregator gateway), respect the curated list instead of also fetching the live `/v1/models` catalog. Without this guard the picker rendered hundreds of online models alongside the user's curated three, swamping the intended selection. The allowlist guard skips the live probe entirely; the existing fall-through to a live probe still runs when no `models:` list is configured. Composes cleanly with [#&#8203;2626](nesquena/hermes-webui#2626) / v0.51.96 — when the live probe is skipped, no `models_endpoint_error` is surfaced (the curated list is the source of truth and probe failures should not show as a user-facing diagnostic in that case).

### [`v0.51.97`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05197--2026-05-20--Release-BU-stage-390--2-PR-batch--startup-session-index-rebuild--config-managed-custom-provider-cards)

[Compare Source](nesquena/hermes-webui@v0.51.96...v0.51.97)

##### Fixed

- **PR [#&#8203;2642](nesquena/hermes-webui#2642 by [@&#8203;dso2ng](https://github.com/dso2ng) — Rebuild the WebUI session index during startup recovery when `_index.json` is missing, even when no `.bak` session restore occurs. Previously the rebuild only ran after a `.bak` restore path, leaving large state directories on a repeated full-scan `/api/sessions` fallback after an index loss. Startup recovery now unconditionally calls `_write_session_index()` when no `_index.json` is present on disk, restoring the O(1) sidebar read path.

##### Added

- **PR [#&#8203;2634](nesquena/hermes-webui#2634 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (closes [#&#8203;2632](nesquena/hermes-webui#2632)) — Show `custom_providers` entries created by `hermes model` (CLI) in Settings → Providers as read-only config-managed provider cards, including their configured models and key status, instead of filtering them out because they are not WebUI-editable API-key providers. The new card variant displays configured models, key-env status (set/unset), and a "config-managed" badge that links to the CLI as the canonical edit surface.

</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/594
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
# Conflicts:
#	CHANGELOG.md
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
# Conflicts:
#	CHANGELOG.md
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