feat: cap pinned sessions from sidebar - #2614
1 commit merged into
Conversation
0608c98 to
9ca846e
Compare
SummaryReviewed the diff at Backend cap
pin_requested = bool(body.get("pinned", True))
if pin_requested and not getattr(s, "pinned", False):
pinned_ids = {
getattr(existing, "session_id", None) for existing in all_sessions()
if getattr(existing, "pinned", False) and not getattr(existing, "archived", False)
}
with LOCK:
pinned_ids.update(
sid for sid, existing in SESSIONS.items()
if getattr(existing, "pinned", False) and not getattr(existing, "archived", False)
)
pinned_ids.discard(body["session_id"])
if len(pinned_ids) >= 3:
return bad(handler, "Up to 3 sessions can be pinned. ...", 400)Two things to flag:
Frontend guards
const pinLimitReached=!session.pinned&&_pinnedSessionCount()>=3;
menu.appendChild(_buildSessionAction(
session.pinned?t('session_unpin'):t('session_pin'),
pinLimitReached?'Only 3 conversations can be pinned':(session.pinned?t('session_unpin_desc'):t('session_pin_desc')),
...
async()=>{
closeSessionActionMenu();
if(pinLimitReached){
if(typeof showToast==='function') showToast('Only 3 conversations can be pinned. Unpin one before pinning another.',3000,'error');
return;
}
...
},
(session.pinned?'is-active':'')+(pinLimitReached?' is-disabled':'')
));
Right-click handler
el.oncontextmenu=(e)=>{
if(readOnly) return;
e.preventDefault();
e.stopPropagation();
...
_openSessionActionMenu(s, actions||el);
};The Test coverage
RecommendationLGTM. Two nits worth considering for follow-up (not blocking):
|
6c60925
# Conflicts: # CHANGELOG.md
… 0.51.96) (#593) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.95` → `0.51.96` | --- ### Release Notes <details> <summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary> ### [`v0.51.96`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05196--2026-05-20--Release-BT-stage-389--8-PR-batch--IPv6-dashboard-link-normalization--configured-title-generation-provider-routing--sidebar-pinned-session-3-cap--external-refresh-sidecar-count-preference--Hermes-overview-docs-relocation--legacy-dedup-timestamp-granularity--custom-provider-models-endpoint-error-surfacing--RuntimeAdapter-Slice-4c-harness-gate-RFC) [Compare Source](nesquena/hermes-webui@v0.51.95...v0.51.96) ##### Fixed - **PR [#​2610](nesquena/hermes-webui#2610 by [@​AJV20](https://github.com/AJV20) — Preserve square brackets around IPv6 hosts when normalizing browser-only dashboard URLs, so links like `http://[::1]:9119` remain valid after saving instead of being mangled into invalid IPv6 forms. Closes the regression introduced by the URL-sanitization path added in [#​2533](nesquena/hermes-webui#2533) / v0.51.95 — bracketed IPv6 hosts now round-trip through the dashboard-link save flow unchanged. - **PR [#​2612](nesquena/hermes-webui#2612 by [@​AJV20](https://github.com/AJV20) — Route WebUI session title generation through the configured `auxiliary.title_generation` provider, model, and base URL when present in config, instead of leaving the auxiliary client to silently fall back to the chat model. Users who configure a smaller/cheaper model for title generation (e.g. a fast 8B model on a separate provider) now have that selection honored end-to-end. - **PR [#​2618](nesquena/hermes-webui#2618 by [@​LumenYoung](https://github.com/LumenYoung) — Prefer the persisted sidecar `message_count` over the session-index stored count during external-refresh polling. The metadata-only `/api/session?messages=0` path now reads `Session._metadata_message_count` when sidecar data is available, so legacy sessions whose state.db retains old rows still trip the external-refresh signal correctly on sidecar updates. Composes cleanly with [#​2604](nesquena/hermes-webui#2604) (the legacy-fallback only applies when the reconciled merged count is zero). - **PR [#​2620](nesquena/hermes-webui#2620 by [@​bengdan](https://github.com/bengdan) — Use second-level timestamp granularity in the legacy message-dedup key. Drops the microsecond fallback in `_normalized_message_timestamp_for_dedup_key()` so transcripts that encode timestamps at different sub-second precisions (e.g. `"10.0"` vs `10.000000`) collapse to the same dedup bucket. Retroactively de-duplicates the dominant failure mode in [#​2616](nesquena/hermes-webui#2616) without requiring an on-disk session rewrite. - **PR [#​2626](nesquena/hermes-webui#2626 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​2540](nesquena/hermes-webui#2540)) — Surface named custom-provider `/models` endpoint failures in the model picker instead of silently showing an empty provider group. `_read_custom_endpoint_models` now returns `(models, error)`, so auth/network/HTTP failures propagate as structured `models_endpoint_error` hints on `/api/models` per affected provider. The composer model picker renders the hint as a quiet disabled-option diagnostic; configured fallback models remain selectable. 124 LOC of new regression coverage spans 401/network-error/5xx failure modes plus frontend hook validation. ##### Added - **PR [#​2614](nesquena/hermes-webui#2614 by [@​Michaelyklam](https://github.com/Michaelyklam) (refs [#​2508](nesquena/hermes-webui#2508)) — Cap sidebar-active pinned sessions at three. Right-clicking a conversation row opens the existing action menu, attempted pins beyond the cap render the menu item as disabled with an explanatory tooltip, and the backend rejects a fourth pin attempt with a structured error so the optimistic frontend can roll back the click. Settles the open question from [#​2508](nesquena/hermes-webui#2508) on whether pin count is bounded — the answer is three, configurable in a future PR if user demand surfaces. ##### Documentation - **PR [#​2619](nesquena/hermes-webui#2619 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​2595](nesquena/hermes-webui#2595)) — Move the long human-facing Hermes comparison document from root `HERMES.md` to `docs/why-hermes.md` so Hermes Agent sessions opened in this repository load `AGENTS.md` as the project-specific assistant guidance instead of the marketing overview. README links now point to the new docs path and a regression test (`tests/test_agent_context_docs.py`) prevents root `HERMES.md` / `.hermes.md` context files from silently reappearing. - **PR [#​2627](nesquena/hermes-webui#2627 by [@​Michaelyklam](https://github.com/Michaelyklam) (refs [#​1925](nesquena/hermes-webui#1925)) — Advance the RuntimeAdapter RFC after the Slice 4b `RunnerRuntimeAdapter` facade shipped in v0.51.94. The RFC now defines the next Slice 4c runner-backend harness gate: feature-flagged runner backend selection, explicit start payload validation, durable status/event observation across WebUI adapter recreation, bounded controls, and a deterministic harness for proving the facade's protocol-translation invariants without requiring the future runner/sidecar to exist. </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/593
…rable (builds on shipped nesquena#2614 3-cap) Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
…➔ 0.51.105) (#613) This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/nesquena/hermes-webui](https://github.com/nesquena/hermes-webui) | patch | `0.51.103` → `0.51.105` | --- ### Release Notes <details> <summary>nesquena/hermes-webui (ghcr.io/nesquena/hermes-webui)</summary> ### [`v0.51.105`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051105--2026-05-21--Release-CC-stage-398--4-PR-batch--hide-suggestions-preference--Docker-agent-version-from-copied-source--runner-local-adapter-selection--configurable-pinned-session-limit) [Compare Source](nesquena/hermes-webui@v0.51.104...v0.51.105) ##### Added - **PR [#​2687](nesquena/hermes-webui#2687 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​2679](nesquena/hermes-webui#2679)) — Settings → Preferences gains a `Hide chat suggestions` toggle (config key `hide_empty_state_suggestions`). The empty new-chat screen normally shows three suggestion buttons as first-class tap targets, which causes accidental taps on mobile. Users who don't want the suggestions can hide them via the preference; the toggle persists across sessions and reloads. Default is OFF (suggestions remain visible) so existing users see no change. - **PR [#​2700](nesquena/hermes-webui#2700 by [@​ai-ag2026](https://github.com/ai-ag2026) — Settings → Preferences gains a `Pinned conversations limit` numeric input. Builds on v0.51.96's [#​2614](nesquena/hermes-webui#2614) 3-cap by making the cap configurable (range 1–99, default 3, validated server-side via `_SETTINGS_INT_RANGES`). Backend validates the new cap on read, surfaces an error if a pin attempt would exceed it, and the right-click menu disables the pin item with an explanatory tooltip when the cap is reached. Default-3 keeps existing users on identical behavior. - **PR [#​2696](nesquena/hermes-webui#2696 by [@​Michaelyklam](https://github.com/Michaelyklam) — RuntimeAdapter slice 4c — feature-flagged runner backend selection. The existing `HERMES_WEBUI_RUNTIME_ADAPTER` env var gains a new `runner-local` mode that wires up a `RunnerRuntimeAdapter` factory and adds a restart/reattach harness gate before the runner backend is used at the dispatcher. No user-visible change in this slice — unset / `legacy-direct` keeps existing behavior intact, and no production caller wires the new adapter yet. The slice exists so future work can land a sidecar runner without changing the runtime contract for existing users. ##### Fixed - **PR [#​2703](nesquena/hermes-webui#2703 by [@​Michaelyklam](https://github.com/Michaelyklam) (closes [#​2691](nesquena/hermes-webui#2691)) — System panel now detects the Hermes Agent version in Docker two-container deployments where the WebUI sees a copied Agent source volume instead of a live git checkout. The new detection cascade reads `VERSION` if present, falls back to the package metadata (`hermes_cli`), and finally to a `.git` describe if either is available, so the System panel reports the right version even when both `VERSION` and `.git` are absent in the copied source. ### [`v0.51.104`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v051104--2026-05-21--Release-CB-stage-397--9-PR-batch--i18n-zh-CNzh-TW-cron-status--geist-contrast-skin-polish--tablet-hardware-Enter--stale-Codex-slash-model-state--SSE-reconnect-jitter--cron-run-inline-expansion--inflight-send-race--new-chat-model-provider-sync--virtualized-sidebar-scroll-clamp-resync--transcript-cache-invalidation-on-same-count-content) [Compare Source](nesquena/hermes-webui@v0.51.103...v0.51.104) ##### Fixed - **PR [#​2690](nesquena/hermes-webui#2690 by [@​laiaman](https://github.com/laiaman) — Correct the zh-CN and zh-Hant translations for the `cron_status_active` label so it reads "enabled / scheduled" (`已启用` / `已啟用`) instead of "running" (`运行中` / `活躍中`). The English source is "active" (enabled, scheduled), and the prior Chinese strings conflated it with the separate `cron_status_running` "currently executing" state, making both labels look identical when a job was both scheduled and not currently firing. - **PR [#​2701](nesquena/hermes-webui#2701 by [@​jasonjcwu](https://github.com/jasonjcwu) — Geist-contrast skin composer polish: force `--user-bubble-text` to `#111` in light mode so typed text is black on the light input background; hide the textarea scrollbar to match the rest of the skin; recolor the send button so it reads correctly against the contrast palette. - **PR [#​2706](nesquena/hermes-webui#2706 by [@​dobby-d-elf](https://github.com/dobby-d-elf) — Tablet (iPad-class) devices with an attached hardware keyboard now send on Enter and newline on Shift+Enter, matching desktop behavior. The prior touch-primary check forced Enter→newline on every touch device, but tablets with hardware keyboards have a physical Shift key and should follow the desktop contract. Detection uses `matchMedia('(pointer:coarse)')` + a `window.visualViewport` height-delta probe (>120px shrink = software keyboard open) so an iPad with hardware keyboard (viewport not shrunk) treats Enter as send, while a phone tapping into the composer (soft keyboard shrinks the viewport) keeps Enter as newline. Falls back to the legacy touch behavior when `visualViewport` is unavailable. - **PR [#​2684](nesquena/hermes-webui#2684 by [@​ai-ag2026](https://github.com/ai-ag2026) — Repair stale `openai/...` slash-qualified model IDs when the active/session provider is `openai-codex`. A stale browser/localStorage selection of `openai/gpt-5` against an `openai-codex` provider previously routed the chat to OpenAI directly instead of through Codex. The cross-provider model-switch resolver now detects the mismatch and re-resolves the model to the matching `codex/...` ID before the request goes out. Explicit OpenRouter slash-qualified selections continue to fast-path through unchanged. - **PR [#​2671](nesquena/hermes-webui#2671 by [@​AJV20](https://github.com/AJV20) (closes [#​2629](nesquena/hermes-webui#2629) + [#​2661](nesquena/hermes-webui#2661)) — Session-list SSE reconnects now use bounded jitter/backoff (each retry delay is `base*0.75 + random*(base*0.35)` where `base = min(30000, 5000 * 2^attempt)`, capped at 30s) instead of a fixed 5-second retry, so tabs that all dropped at the same time (server restart, network drop) don't all retry in lockstep. Expanded cron run rows now render the full output inline immediately on click; the truncated preview remains only for collapsed rows, and the full-output fallback no longer drops content when Markdown rendering is unavailable. - **PR [#​2689](nesquena/hermes-webui#2689 by [@​ai-ag2026](https://github.com/ai-ag2026) — Preserve the optimistic in-flight message array across the `/api/chat/start` await window so a fast back-to-back send doesn't clear the user's message before the stream ID arrives. The fix snapshots the inflight entry before the await, recreates it if a sidebar/session refresh pruned it during that window, and skips stale-inflight cleanup for the submitting session until a stream ID is bound. Regression test covers the race. - **PR [#​2674](nesquena/hermes-webui#2674 by [@​AJV20](https://github.com/AJV20) — Resync the new-chat model picker when the server-created session has the same model ID as the current dropdown but a different provider. New conversations now resync to the configured default model provider instead of inheriting a stale persisted picker selection (e.g. `openai/gpt-5` from a previous session). Without this, the dropdown text matched the new session's model, but the provider attribute still pointed at the stale choice. - **PR [#​2688](nesquena/hermes-webui#2688 by [@​ai-ag2026](https://github.com/ai-ag2026) — Resync the virtualized session sidebar after restoring a saved scroll position if the browser clamps or rejects that scroll position. Without this, date-group headers could render without their session rows beneath them until the user manually scrolled or a later refresh recomputed the virtual window. Regression test pins the recompute path. - **PR [#​2692](nesquena/hermes-webui#2692 by [@​ai-ag2026](https://github.com/ai-ag2026) (refs [#​2613](nesquena/hermes-webui#2613)) — Invalidate the transcript render cache on same-count content changes, not just on count changes. The prior cache key was `(message_count, render_window_size)`, which silently reused a cached transcript whenever a same-count edit produced visibly different content (e.g. a tool retry that replaces a single assistant message with corrected text). The new cache signature folds a content hash into the key so any visible change forces a fresh render. Regression test asserts cache-bust on same-count content swap. </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/613
# Conflicts: # CHANGELOG.md
…rable (builds on shipped nesquena#2614 3-cap) Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
# Conflicts: # CHANGELOG.md
…rable (builds on shipped nesquena#2614 3-cap) Co-authored-by: ai-ag2026 <ai-ag2026@users.noreply.github.com>
Thinking Path
What Changed
/api/session/pinthat rejects a fourth active pinned conversation until one is unpinned.Why It Matters
Verification
env -u HERMES_CONFIG_PATH -u HERMES_WEBUI_HOST /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_issue2508_session_pin_cap.py tests/test_sprint12.py::test_pin_session tests/test_sprint12.py::test_unpin_session tests/test_sprint12.py::test_pinned_in_session_list tests/test_issue856_pinned_indicator_layout.py -q— 15 passedenv -u HERMES_CONFIG_PATH -u HERMES_WEBUI_HOST /home/michael/.hermes/hermes-agent/venv/bin/python -m py_compile api/routes.pynode --check static/sessions.jsgit diff --checkUI Media
Risks / Follow-ups
Refs #2508
Model Used
AI-assisted change with repository inspection, targeted editing, and shell-based test verification.