fix: guard new conversation cold-start clicks - #2528
1 commit merged into
Conversation
41baac4 to
aeef101
Compare
aeef101 to
b76d698
Compare
SummaryReading The shape — module-level Code referenceGuard entry/exit at async function newSession(flash, options={}){
if(_newSessionInFlight){
if(typeof showToast==='function') showToast(_newSessionPendingText(),1500);
return _newSessionInFlight;
}
_setNewSessionPending(true);
_newSessionInFlight=(async()=>{
// ... existing create body
})();
try{
return await _newSessionInFlight;
}finally{
_newSessionInFlight=null;
_setNewSessionPending(false);
}
}The composer-status helper at function _setNewSessionPending(pending){
// ...
const pendingText=_newSessionPendingText();
if(pending){
setComposerStatus(pendingText);
}else if(statusEl&&statusEl.textContent===pendingText){
setComposerStatus('');
}
}That's a good detail — it avoids racing against legitimate status writes from the inner block. Diagnosis / RecommendationThe change is structurally fine. A few notes: 1. Overlap with PR #2519. That PR is open against the same issue and also lands a module-level promise +
Neither is strictly superior. Either way, both fixes shouldn't land. 2. The static test at 3. The await/finally placement is correct. 4. No error toast on rejection. If VerificationThe CHANGELOG entry is release-note-ready. The i18n keys ( The main open question is the coordination with #2519. Both PRs are correct fixes; the issue is they shouldn't both land. |
4589dbe
# Conflicts: # CHANGELOG.md
… 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 [#​2286](nesquena/hermes-webui#2286 by [@​junjunjunbong](https://github.com/junjunjunbong) (refs [#​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 [#​2459](nesquena/hermes-webui#2459 by [@​franksong2702](https://github.com/franksong2702) (closes [#​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 [#​2522](nesquena/hermes-webui#2522 by [@​Michaelyklam](https://github.com/Michaelyklam) (refs [#​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 [#​2515](nesquena/hermes-webui#2515 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​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 [#​2512](nesquena/hermes-webui#2512 by [@​dso2ng](https://github.com/dso2ng) (refs [#​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 [#​2528](nesquena/hermes-webui#2528 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​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 [#​2530](nesquena/hermes-webui#2530 by [@​franksong2702](https://github.com/franksong2702) (refs [#​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 [#​2294](nesquena/hermes-webui#2294 by [@​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 [#​2511](nesquena/hermes-webui#2511 by [@​franksong2702](https://github.com/franksong2702) (refs [#​2502](nesquena/hermes-webui#2502) / [#​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 [#​2509](nesquena/hermes-webui#2509 by [@​Michaelyklam](https://github.com/Michaelyklam) (refs [#​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
…#2518 follow-up) The frontend in-flight guard (PR nesquena#2528) made repeated + clicks safe but left a single cold click waiting 3-4s behind get_available_models(): newSession() carried the dropdown's model_provider as reqBody.model_provider. When the dropdown option has no data-provider attribute (or its value is 'default') and the persisted state predates provider tracking, newModelState.model_provider is null. The server's fast path in _resolve_compatible_session_model_state requires both model AND a truthy model_provider; without that, the request falls into the cold catalog rebuild. The catalog warms after the first response, so subsequent clicks are fast. newSession() now falls back through a 3-step chain: 1. newModelState.model_provider (explicit picker) 2. window._activeProvider (boot-hydrated active route) 3. S.session.model_provider (previous session) Whenever a usable default exists, the request hits the server's fast path and stays out of get_available_models() entirely. The slow path remains the safety net for genuinely provider-less clients. Closes the open follow-up from nesquena#2518. Tests: - tests/test_issue2518_active_provider_fallback.py (new, 7 cases): source shape (fallback present, prev-session present, chain order, issue reference) + end-to-end (fast path on real + slow path still fires without provider). - tests/test_new_chat_default_model_frontend.py: rewrote test_new_session_posts_picker_model_before_server_default from a literal-string snapshot into a behavior-contract assertion (chain members + ordering), per AGENTS.md change-detector guidance.
…#2518 follow-up) The frontend in-flight guard (PR nesquena#2528) made repeated + clicks safe but left a single cold click waiting 3-4s behind get_available_models(): newSession() carried the dropdown's model_provider as reqBody.model_provider. When the dropdown option has no data-provider attribute (or its value is 'default') and the persisted state predates provider tracking, newModelState.model_provider is null. The server's fast path in _resolve_compatible_session_model_state requires both model AND a truthy model_provider; without that, the request falls into the cold catalog rebuild. The catalog warms after the first response, so subsequent clicks are fast. newSession() now falls back through a 3-step chain: 1. newModelState.model_provider (explicit picker) 2. window._activeProvider (boot-hydrated active route) 3. S.session.model_provider (previous session) Whenever a usable default exists, the request hits the server's fast path and stays out of get_available_models() entirely. The slow path remains the safety net for genuinely provider-less clients. Closes the open follow-up from nesquena#2518. Tests: - tests/test_issue2518_active_provider_fallback.py (new, 7 cases): source shape (fallback present, prev-session present, chain order, issue reference) + end-to-end (fast path on real + slow path still fires without provider). - tests/test_new_chat_default_model_frontend.py: rewrote test_new_session_posts_picker_model_before_server_default from a literal-string snapshot into a behavior-contract assertion (chain members + ordering), per AGENTS.md change-detector guidance.
…#2518 follow-up) The frontend in-flight guard (PR nesquena#2528) made repeated + clicks safe but left a single cold click waiting 3-4s behind get_available_models(): newSession() carried the dropdown's model_provider as reqBody.model_provider. When the dropdown option has no data-provider attribute (or its value is 'default') and the persisted state predates provider tracking, newModelState.model_provider is null. The server's fast path in _resolve_compatible_session_model_state requires both model AND a truthy model_provider; without that, the request falls into the cold catalog rebuild. The catalog warms after the first response, so subsequent clicks are fast. newSession() now falls back through a 3-step chain: 1. newModelState.model_provider (explicit picker) 2. window._activeProvider (boot-hydrated active route) 3. S.session.model_provider (previous session) Whenever a usable default exists, the request hits the server's fast path and stays out of get_available_models() entirely. The slow path remains the safety net for genuinely provider-less clients. Closes the open follow-up from nesquena#2518. Tests: - tests/test_issue2518_active_provider_fallback.py (new, 7 cases): source shape (fallback present, prev-session present, chain order, issue reference) + end-to-end (fast path on real + slow path still fires without provider). - tests/test_new_chat_default_model_frontend.py: rewrote test_new_session_posts_picker_model_before_server_default from a literal-string snapshot into a behavior-contract assertion (chain members + ordering), per AGENTS.md change-detector guidance.
…llback Closes the open follow-up from nesquena#2518 - addresses the cross-provider regression flagged in PR nesquena#3410 review: when the persisted state carries a stale foreign-slug model (e.g. "gemini/gemini-2.5") from a session served by a different provider than the now-active one, the window._activeProvider fallback would attach the wrong provider and let _resolve_compatible_session_model_state's fast path pass it through without consulting the catalog - silently re-pointing the new session at the wrong backend. The new client guard wraps the active-provider fallback in a _bareModel ternary (rejects '/' and '@' prefixes) so slash-qualified and @-qualified models keep model_provider=null on the wire and the slow path's cross-provider normalization still runs. Also drops a vestigial mid-chain '||null' no-op. Adds 6 regression tests in test_issue2518_active_provider_fallback.py (test_slash_qualified_model_keeps_active_provider_behind_guard, test_at_qualified_model_also_keeps_active_provider_behind_guard, test_explicit_picker_provider_still_wins, test_no_op_null_terminal_in_fallback_chain, test_slash_slug_keeps_provider_null_in_wire_shape, test_bare_model_uses_active_provider_when_no_picker). Behavior-contract assertions, not source-string pins, so future refactors of the same contract still satisfy them. Builds on nesquena#2528 (in-flight guard) and nesquena#1855 (fast path). PR body draft: docs/pr-media/2518/PR_BODY.md
…#2518 follow-up) The frontend in-flight guard (PR nesquena#2528) made repeated + clicks safe but left a single cold click waiting 3-4s behind get_available_models(): newSession() carried the dropdown's model_provider as reqBody.model_provider. When the dropdown option has no data-provider attribute (or its value is 'default') and the persisted state predates provider tracking, newModelState.model_provider is null. The server's fast path in _resolve_compatible_session_model_state requires both model AND a truthy model_provider; without that, the request falls into the cold catalog rebuild. The catalog warms after the first response, so subsequent clicks are fast. newSession() now falls back through a 3-step chain: 1. newModelState.model_provider (explicit picker) 2. window._activeProvider (boot-hydrated active route) 3. S.session.model_provider (previous session) Whenever a usable default exists, the request hits the server's fast path and stays out of get_available_models() entirely. The slow path remains the safety net for genuinely provider-less clients. Closes the open follow-up from nesquena#2518. Tests: - tests/test_issue2518_active_provider_fallback.py (new, 7 cases): source shape (fallback present, prev-session present, chain order, issue reference) + end-to-end (fast path on real + slow path still fires without provider). - tests/test_new_chat_default_model_frontend.py: rewrote test_new_session_posts_picker_model_before_server_default from a literal-string snapshot into a behavior-contract assertion (chain members + ordering), per AGENTS.md change-detector guidance.
…llback Closes the open follow-up from nesquena#2518 - addresses the cross-provider regression flagged in PR nesquena#3410 review: when the persisted state carries a stale foreign-slug model (e.g. "gemini/gemini-2.5") from a session served by a different provider than the now-active one, the window._activeProvider fallback would attach the wrong provider and let _resolve_compatible_session_model_state's fast path pass it through without consulting the catalog - silently re-pointing the new session at the wrong backend. The new client guard wraps the active-provider fallback in a _bareModel ternary (rejects '/' and '@' prefixes) so slash-qualified and @-qualified models keep model_provider=null on the wire and the slow path's cross-provider normalization still runs. Also drops a vestigial mid-chain '||null' no-op. Adds 6 regression tests in test_issue2518_active_provider_fallback.py (test_slash_qualified_model_keeps_active_provider_behind_guard, test_at_qualified_model_also_keeps_active_provider_behind_guard, test_explicit_picker_provider_still_wins, test_no_op_null_terminal_in_fallback_chain, test_slash_slug_keeps_provider_null_in_wire_shape, test_bare_model_uses_active_provider_when_no_picker). Behavior-contract assertions, not source-string pins, so future refactors of the same contract still satisfy them. Builds on nesquena#2528 (in-flight guard) and nesquena#1855 (fast path). PR body draft: docs/pr-media/2518/PR_BODY.md
# Conflicts: # CHANGELOG.md
# Conflicts: # CHANGELOG.md
Thinking Path
POST /api/session/newwaits behind model/provider catalog resolution.What Changed
newSession()promise so repeated New Conversation triggers reuse the same create request until it settles.aria-busy="true"while creation is pending.Creating new conversation…) during the cold-create window.Why It Matters
This makes the button feel responsive during cold model/provider catalog resolution and prevents accidental duplicate blank sessions from rapid repeated clicks.
Closes #2518
Verification
env -u HERMES_CONFIG_PATH -u HERMES_WEBUI_HOST /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_issue2518_new_session_inflight.py tests/test_1432_newchat_and_1423_profile_input.py tests/test_profile_default_workspace_823.py -q— 20 passedenv -u HERMES_CONFIG_PATH -u HERMES_WEBUI_HOST /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_issue2518_new_session_inflight.py tests/test_chinese_locale.py tests/test_japanese_locale.py tests/test_korean_locale.py tests/test_russian_locale.py tests/test_spanish_locale.py -q— 29 passednode --check static/sessions.jsnode --check static/i18n.jsgit diff --check127.0.0.1:18818UI Media
Before / idle New Conversation button:
After / pending create state:
Risks / Follow-ups
newSession()intentionally share the same in-flight promise, so a slash command or shortcut fired during the pending window will land on the same new session rather than creating another duplicate.Model Used
AI-assisted change with repository inspection, targeted editing, shell-based test verification, and isolated browser screenshot capture.