Much faster agent profile switch and app startup - #2633
5 commits merged into
Conversation
SummaryReading PR head The shape of the change is sound: apply profile defaults from Code referenceThe new lazy-hydration trigger at 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 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 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. Diagnosis1. Backend payload addition. 2. Workspace duplicate-write elimination. The 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 3. FLIP animation correctness. 4. Generation counter. Concerns1. Boot-time synthetic option lacks 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 = window._activeProvider || '';right before 2. 3. The // 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 ( 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 ( RecommendationApprove 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 |
3c91e33 to
fd7212b
Compare
|
moving to draft while I iron out a few issues |
|
|
@nesquena-hermes this is ready to review again. Tested and addressed review comments |
f4a7989
# Conflicts: # CHANGELOG.md
… 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 [#​2663](nesquena/hermes-webui#2663 by [@​Fail-Safe](https://github.com/Fail-Safe) (closes [#​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 [#​2662](nesquena/hermes-webui#2662 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​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 [#​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 [#​2625](nesquena/hermes-webui#2625 by [@​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 [#​2636](nesquena/hermes-webui#2636 by [@​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 [#​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 [#​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 [#​2636](nesquena/hermes-webui#2636). - CSS contrast fix for [#​2636](nesquena/hermes-webui#2636) — `color: #​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 [#​2636](nesquena/hermes-webui#2636) maintainer additions (profile-switch wiring, chat/settings server-side strip, a11y switch role). ##### UX approval PR [#​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 [#​2615](nesquena/hermes-webui#2615 by [@​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 [#​2633](nesquena/hermes-webui#2633 by [@​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 [#​2637](nesquena/hermes-webui#2637 by [@​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 [#​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 [#​2638](nesquena/hermes-webui#2638 by [@​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 [#​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 [#​2647](nesquena/hermes-webui#2647 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​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 [#​2650](nesquena/hermes-webui#2650 by [@​starship-s](https://github.com/starship-s) — Forward the configured `auxiliary.title_generation.api_key` for config-derived title routes. Completes [#​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 [#​2651](nesquena/hermes-webui#2651 by [@​LumenYoung](https://github.com/LumenYoung) (refs [#​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 [#​2643](nesquena/hermes-webui#2643 by [@​arshkumarsingh](https://github.com/arshkumarsingh) (closes [#​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 [#​2640](nesquena/hermes-webui#2640 by [@​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 [#​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 [#​2642](nesquena/hermes-webui#2642 by [@​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 [#​2634](nesquena/hermes-webui#2634 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​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
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
Summary
No new or changed functionality is intended. This is scoped to perceived performance and smoothness only.
Measurements
Review follow-up
Tests