Skip to content

feat(quota-chip): add Settings toggle, flip default to off - #2413

Closed
nesquena-hermes wants to merge 1 commit into
masterfrom
feat/quota-chip-settings-toggle
Closed

nesquena-hermes wants to merge 1 commit into
masterfrom
feat/quota-chip-settings-toggle

Conversation

@nesquena-hermes

Copy link
Copy Markdown
Collaborator

feat(quota-chip): add Settings toggle, flip default to off

Direct follow-up to the v0.51.78 stage-371 release that shipped the ambient provider quota chip (@ai-ag2026's #2082, hardened with the @media (max-width:1399.98px) clutter gate). Adds a Settings → Preferences checkbox to opt into the chip, with default off.

Why this is a follow-up not a rework

The chip itself is well-built — fetch path, rendering, tab-visibility refresh, and the wide-desktop @media gate all work as designed. The only piece missing was user agency: even on wide desktops where the chip fits cleanly, a user who doesn't want an always-on quota indicator in their composer couldn't turn it off without inspecting devtools.

This PR adds the per-user opt-in. The default flips from "visible whenever you're on wide desktop with quota data" to "hidden everywhere unless you explicitly enable it in Settings."

Wiring

  • api/config.py"show_quota_chip": False in _SETTINGS_DEFAULTS, added to _SETTINGS_BOOL_KEYS (auto-flows into _SETTINGS_ALLOWED_KEYS)
  • static/boot.jswindow._showQuotaChip initialized from settings, default-false on fetch-failure
  • static/panels.js — payload + body + _applySavedSettingsUi propagation, live refresh on toggle change
  • static/ui.jsrenderProviderQuotaIndicator and refreshProviderQuotaIndicator both short-circuit when disabled. Refresh skips the /api/provider/quota fetch entirely, so the chip-disabled state has zero runtime cost.
  • static/index.html — new Settings field after "Show token usage", with description text explaining the wide-display gate ("Default off. Only visible on wide displays (≥1400px) when enabled, to keep the composer uncluttered on laptop and standard desktop widths.")
  • static/i18n.jssettings_label_quota_chip + settings_desc_quota_chip in 11 locales

Tests

tests/test_quota_chip_settings_toggle.py — 6 regression cases pin:

  • Settings field present (id, data-i18n labels)
  • show_quota_chip default-False in _SETTINGS_DEFAULTS and _SETTINGS_BOOL_KEYS
  • renderProviderQuotaIndicator early-hides BEFORE the existing text-computation path
  • refreshProviderQuotaIndicator short-circuits BEFORE the fetch
  • Boot initializes window._showQuotaChip from settings, defaults to false on settings-fetch failure
  • panels.js full round-trip (payload, body, load, save, live-refresh)
  • All 11 locale strings present

5790 tests pass on this branch (up from 5784 on master after stage-371 merged).

UX evidence

Self-verified at 1920×1080 (wide desktop) before opening this PR:

  • Default state: composer footer clean, no green chip (was visible by default before this PR)
  • Settings panel: new checkbox below "Show token usage", unchecked by default, description text fully readable
  • Toggled checkbox ON, then re-rendered: chip "OpenRouter $2.40" appears in the composer footer between model picker and reasoning chip, as designed
  • 1280/1440 viewports already hidden via existing @media (max-width:1399.98px) rule — no additional viewport work needed

Adds a 'Show provider quota chip in composer' checkbox in Settings →
Preferences. Default off. When disabled (the default), the chip is
hidden at all viewport sizes and the /api/provider/quota fetch is
skipped entirely. When enabled, the existing @media (max-width:1399.98px)
hide rule from stage-371 still gates the chip to wide desktops only.

Per Nathan's directive 2026-05-16, immediately after stage-371 shipped
the chip with master at default-visible:
  'I want to be able to choose whether I want to see the provider
   quota chip or not, and I want it to be default off.'

Wiring:
- api/config.py: 'show_quota_chip': False in _SETTINGS_DEFAULTS, added
  to _SETTINGS_BOOL_KEYS so it round-trips through the writable allow-list.
- static/boot.js: window._showQuotaChip initialized from settings, falls
  back to false when the initial settings fetch fails.
- static/panels.js: payload + body read/write, _applySavedSettingsUi
  propagates the value to window._showQuotaChip, live refresh fires
  refreshProviderQuotaIndicator() on toggle for immediate visual feedback.
- static/ui.js: renderProviderQuotaIndicator hides chip before any other
  render logic when disabled; refreshProviderQuotaIndicator short-circuits
  the fetch entirely when disabled (no wasted API calls).
- static/index.html: new Settings field after 'Show token usage', describes
  the wide-display gate so users understand why it might not show at
  laptop widths.
- static/i18n.js: settings_label_quota_chip + settings_desc_quota_chip
  in 11 locales (en, it, ja, ru, es, de, zh, zh-Hant, pt, ko, fr).

Tests: tests/test_quota_chip_settings_toggle.py — 6 cases pin the toggle
invariants: Settings field present, default-False in config, ui.js render
and refresh both short-circuit when disabled, boot initializes from
settings + default-false on fetch failure, panels.js full round-trip,
all 11 locales have both strings.

5790 tests pass on this branch (up from 5784 on master, 6 new tests).
@nesquena-hermes

Copy link
Copy Markdown
Collaborator Author

Ready for independent review (self-built PR, nesquena APPROVED required before merge). 161 LOC additive across the standard settings-round-trip wiring + 6 regression tests + 11 locale strings. UX self-verified at 1920×1080 (default-hidden state, Settings checkbox unchecked, post-toggle chip appearance). 1280/1440 viewports are already hidden via the existing stage-371 @media rule and don't need re-screenshot — composer at those widths is unchanged from current master.

CI green expected — pytest passed 5790 / 0-failed locally.

Once approved + CI green, this is eligible to batch with #2406 / #2407 / #2408 (Michaelyklam's three queued contributor PRs once their independent reviews land) into stage-372 — or ship as its own single-PR release lane, whichever lands its review first.

@nesquena nesquena left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — end-to-end ✅ (clean APPROVE, no fix pushed)

What this ships

161-line follow-up to the v0.51.78 stage-371 release that shipped the ambient quota chip (@ai-ag2026's #2082). Adds a Settings → Preferences opt-in toggle and flips the default from on-when-wide to off everywhere. Net effect for existing users: the chip disappears until they explicitly turn it on.

Touches: api/config.py (+2), static/boot.js (+2), static/i18n.js (+22 in 11 locales), static/index.html (+7), static/panels.js (+20/-3), static/ui.js (+16), tests/test_quota_chip_settings_toggle.py (+92 new, 6 tests). CI green on 3.11/3.12/3.13.

Traced against upstream hermes-agent

Pulled a fresh tarball. No cross-tool surfaceshow_quota_chip lives in webui-internal _SETTINGS_DEFAULTS at api/config.py:4022 (not in config.yaml). The CLI doesn't read this preference. /api/provider/quota is also webui-only.

End-to-end trace

Server settings layer

  • api/config.py:4022"show_quota_chip": False added to _SETTINGS_DEFAULTS.
  • api/config.py:4156"show_quota_chip" added to _SETTINGS_BOOL_KEYS, which auto-flows into _SETTINGS_ALLOWED_KEYS (the writable allow-list). Round-trip safe.

Boot path

  • static/boot.js:1390window._showQuotaChip = s.show_quota_chip === true (strict). Settings load success path.
  • static/boot.js:1456window._showQuotaChip = false in the settings-fetch-failed defaults block. Safe-fail.
  • static/boot.js:1532refreshProviderQuotaIndicator() called at boot. With default off, the function short-circuits to a no-op (no fetch).

Render short-circuit at static/ui.js:675-687

function renderProviderQuotaIndicator(status){
  const chip = $('providerQuotaChip');
  const label = $('providerQuotaChipLabel');
  if(!chip||!label) return;
  if(window._showQuotaChip !== true){
    chip.hidden = true;
    label.textContent = '';
    chip.removeAttribute('title');
    return;
  }
  // ... existing render logic
}

Guard runs before _providerQuotaIndicatorText(status) is computed. Test pins this ordering via line-index comparison.

Refresh short-circuit at static/ui.js:699-707

async function refreshProviderQuotaIndicator(){
  if(window._showQuotaChip !== true){
    const chip = $('providerQuotaChip');
    if(chip){ chip.hidden=true; chip.removeAttribute('title'); }
    return;
  }
  // ... existing in-flight guard + fetch
}

Skips the /api/provider/quota fetch entirely when disabled — chip-off users have zero runtime cost. The visibility-change listener at static/ui.js:719 also flows through this gated entry point.

Settings panel round-trip

  • Load: static/panels.js:5500-5510 — checkbox initialized from settings.show_quota_chip === true. Change listener updates window._showQuotaChip AND calls refreshProviderQuotaIndicator() for live refresh (no reload required).
  • Save payload (autosave): static/panels.js:5258-5259payload.show_quota_chip = showQuotaChipCb.checked.
  • Save body (explicit save): static/panels.js:6376, 6395body.show_quota_chip = showQuotaChip === true.
  • _applySavedSettingsUi at static/panels.js:6278-6281window._showQuotaChip = showQuotaChip === true propagated post-save.

HTML field

  • static/index.html:1024-1030 — new <div class="settings-field"> placed AFTER "Show token usage" (a sibling boolean toggle), BEFORE "Show TPS". Visual grouping with related display-toggles is sensible.

i18n

  • settings_label_quota_chip + settings_desc_quota_chip in 11 locales. Description copy explicitly mentions the wide-display gate ("Only visible on wide displays (≥1400px) when enabled") so users understand the relationship to the @media rule.

Other audit — things that are correct already

Security

  • No HTML insertion of user data. The checkbox value is a boolean round-tripped through /api/settings. The render+refresh helpers only call DOM attribute setters (textContent, title, hidden).
  • Settings allow-list (_SETTINGS_ALLOWED_KEYS via _SETTINGS_BOOL_KEYS) is the canonical write boundary; this PR correctly extends both.

Type-strict gates

  • All four checks use === true (strict equality). Defends against 'true' string, 1, truthy objects, etc. — useful when localStorage or stale body.show_quota_chip could leak a non-boolean.

Zero-cost when disabled

  • refreshProviderQuotaIndicator early-returns BEFORE the in-flight flag and BEFORE the network call. Disabled users never make /api/provider/quota requests, never burn server quota-probe semaphores (#2030's _account_usage_probe_semaphore).

Default-safe failure paths

  • Settings fetch fails in boot.js? window._showQuotaChip = false in the catch block.
  • Property missing entirely from /api/settings response? s.show_quota_chip === true returns false for undefined.
  • Settings fetch succeeds but show_quota_chip is 'true' (string)? Strict equality fails → false. Defensive.

Visibility-change re-fire

  • The visibilitychange listener at static/ui.js:719 fires refreshProviderQuotaIndicator() when tab becomes visible. With chip disabled, this is a cheap no-op (early-return on _showQuotaChip !== true).

No conflict with #2082's existing chip behavior

  • The original chip rendering, fetch path, and @media (max-width:1399.98px) clutter gate are all preserved. This PR adds a guard upstream of all of them.

Behavioural harness (Node)

Case 1: default off — render with status, no fetch         ✓ chip stays hidden
Case 2: default off — refresh skips fetch                  ✓ fetchCount=0
Case 3: enabled — render shows chip                        ✓ label="P 50%"
Case 4: enabled — refresh fires fetch                      ✓ fetchCount=1
Case 5: stringy truthy non-boolean — strict === rejects    ✓ chip hidden
Case 6: toggle off mid-stream — refresh hides immediately  ✓ second refresh no-op

6/6 cases verify the gate behavior, ordering, strict-equality defense, and toggle-off responsiveness.

Edge-case trace

Scenario Expected Actual
Fresh install, default off chip hidden, no fetch ✅ harness
User toggles on in Settings live refresh, chip appears ✅ harness + listener
User toggles off mid-session next refresh hides + skips fetch ✅ harness
Settings fetch fails on boot _showQuotaChip = false ✅ test + diff line 1456
Localstorage stale 'true' (string) rejected by strict === ✅ harness
visibilitychange fires while disabled cheap no-op ✅ gated entry point
Save+reload round-trip state persists ✅ allow-list + boot read
Cross-tool: CLI reads same config.yaml unaffected (webui-only setting)
@1399.98px viewport, enabled hidden by @media gate ✅ pre-existing CSS
≥1400px viewport, enabled, no quota data hidden by inner render branch ✅ pre-existing logic

Tests

  • PR-targeted: 6/6 pass (tests/test_quota_chip_settings_toggle.py).
  • Full suite (Python 3.14, local): 5669 passed, 63 skipped, 3 xpassed, 0 failed (ignoring test_docker_env_readonly_vars.py macOS bash 3.2 baseline + test_ctl_script.py).
  • CI: 3.11/3.12/3.13 all green.
  • Behavioural harness: 6/6 gate scenarios verified.

Minor observations (non-blocking)

  1. Default flip is a UX regression for early adopters of #2082. Anyone who liked the ambient chip from v0.51.78 will see it disappear on update. The Settings field is discoverable via Preferences but won't be obvious unless they specifically look for it. CHANGELOG copy (not in this PR's diff) will need to call this out — "default behavior changed, opt in via Settings → Preferences." Not a blocker; matches the Telegram directive.
  2. Settings field placement. Placed between "Show token usage" and "Show TPS" — sensible grouping with other display toggles. If/when more chip-style toggles arrive, consider a <section>-level grouping ("Composer chrome" / "Status indicators") to scale. Pre-existing layout pattern; not in scope.
  3. _applySavedSettingsUi signature growth. The opts object now has 11 keys (sendKey, showTokenUsage, showQuotaChip, showTps, fadeTextEffect, showCliSessions, theme, skin, language, sidebarDensity, fontSize). At some point this should be a destructured object passed by reference rather than enumerated. Pre-existing pattern; not in this PR's scope.
  4. No regression test for the visibility-change listener. The 6 new tests cover render + refresh + boot + panels + i18n + config. The visibilitychange flow is tested transitively because it calls refreshProviderQuotaIndicator() which IS tested. Acceptable; pre-existing #2082 didn't cover it either.

Recommendation

Approved. Clean opt-in toggle with proper zero-cost short-circuiting when disabled. Strict equality defense, 6 regression tests, behavioural harness confirms all gate transitions. Default flip honors the maintainer's directive while preserving the chip's value for users who explicitly opt in.

✅ Parked at approval — ready for the release agent's merge/tag pipeline.

@nesquena-hermes

Copy link
Copy Markdown
Collaborator Author

Shipped via stage-372 / PR #2414 / v0.51.79 (Release BC). Attribution preserved in the squashed stage commit and the v0.51.79 CHANGELOG entry. Thank you!

@nesquena-hermes
nesquena-hermes deleted the feat/quota-chip-settings-toggle branch May 16, 2026 23:13
pull Bot pushed a commit to TKaxv-7S/hermes-webui that referenced this pull request May 16, 2026
eleboucher pushed a commit to eleboucher/homelab that referenced this pull request May 17, 2026
… 0.51.82) (#528)

This PR contains the following updates:

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

---

### Release Notes

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

### [`v0.51.82`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05182--2026-05-17--Release-BF-stage-375--2-PR-batch--table-renderer-pipe-protection--Catppuccin-appearance-skin)

[Compare Source](nesquena/hermes-webui@v0.51.81...v0.51.82)

##### Added

- **PR [#&#8203;2432](nesquena/hermes-webui#2432 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (closes [#&#8203;2426](nesquena/hermes-webui#2426)) — Add a Catppuccin skin to Appearance settings. The single opt-in skin maps light mode to Catppuccin Latte and dark mode to Catppuccin Mocha, using Mauve as the accent while preserving the existing theme/skin persistence and no-build-step architecture.

##### Fixed

- **PR [#&#8203;2428](nesquena/hermes-webui#2428 by [@&#8203;bengdan](https://github.com/bengdan) — Protect pipes inside parens / brackets / braces from naive `split('|')` in the Markdown table renderer. Cells like `` `(a|b)` ``, `` `Union[int|float]` ``, `` `(a|b|c)` ``, and `` `Union[int|float|str]` `` now stay in a single column instead of mis-splitting. The fix uses an iterative `_protectPipes` loop so all pipes inside one bracket pair are caught, not just the first. Also adds a `$...$` guard so a KaTeX inline-math span straddling `|` column separators is left alone instead of being stashed as math. Stage-fix on the contributor branch (a) swapped the literal `}` glyphs in the regex character classes for `\x7d` hex escapes (semantically identical, but the JS source no longer carries bare close-brace glyphs that confused the brace-counting `extractFunc` in `tests/test_renderer_js_behaviour.py`); (b) dropped a stray apostrophe stop that would have mis-split `('a'|'b')`-style string-literal unions; (c) dropped angle brackets `<` / `>` from the protected-bracket set, after Opus advisor flagged that `| x < 5 | y > 10 |` would otherwise collapse into a single cell (comparison-operator usage dominates content-grouping usage in real LLM table output); and (d) added `tests/test_issue2428_table_pipe_protection.py` with 12 regression cases covering single-pipe, multi-pipe-in-brackets, apostrophes-with-pipes, the KaTeX-in-table guard, and the angle-bracket comparison-operator case.

### [`v0.51.81`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05181--2026-05-17--Release-BE-stage-374--6-PR-batch--cost-history-POSIX-lock--prompt-cache-tokens--Plugins-panel-i18n--pending-placeholder-chat--journal-replay-partial-recovery--default-off-RuntimeAdapter-Slice-2-seam)

[Compare Source](nesquena/hermes-webui@v0.51.80...v0.51.81)

##### Added

- **PR [#&#8203;2424](nesquena/hermes-webui#2424 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (refs [#&#8203;1925](nesquena/hermes-webui#1925)) — Add the default-off `RuntimeAdapter` Slice 2 seam. `HERMES_WEBUI_RUNTIME_ADAPTER=legacy-journal` now routes chat start through a `LegacyJournalRuntimeAdapter` facade over the existing legacy streaming path, while the default remains `legacy-direct`. The new adapter interface/payload classes expose start/observe/status/cancel/approval/clarify methods and delegate controls to existing handlers without introducing a runner, sidecar, new process-local queues, cached agents, cancellation registries, or callback registries.
- **PR [#&#8203;2421](nesquena/hermes-webui#2421 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2419](nesquena/hermes-webui#2419)) — Surface provider prompt-cache read/write tokens in WebUI usage displays. Cache-miss cost issues are now visible in the context tooltip and per-turn usage footer; counters carry through session persistence, SSE usage payloads, and live snapshots so deltas remain accurate across the active turn.
- **PR [#&#8203;2425](nesquena/hermes-webui#2425 by [@&#8203;mccxj](https://github.com/mccxj) — Wire Settings → Plugins panel into the existing i18n system. Panel title, description, empty state, and per-plugin labels (hooks, enabled/disabled, load failures) now respect the user's language preference; 10 new keys ship in English with `TODO: translate` placeholders in 9 additional locales.

##### Fixed

- **PR [#&#8203;2418](nesquena/hermes-webui#2418 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2402](nesquena/hermes-webui#2402)) — OpenRouter cost-history snapshot updates now take a provider-specific POSIX file lock around the read-modify-write cycle, preserving the existing process-local lock while preventing lost snapshot updates if WebUI is deployed with multiple worker processes sharing one Hermes home/state directory.
- **PR [#&#8203;2431](nesquena/hermes-webui#2431 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2429](nesquena/hermes-webui#2429)) — Chat sends now render the assistant-side pending `Thinking…` placeholder immediately after the user turn is echoed, before `/api/chat/start` returns a stream id or the first SSE event arrives. The existing stale-stream guard remains in place for ordinary reasoning updates — only the explicit pre-stream placeholder path is allowed through.
- **PR [#&#8203;2427](nesquena/hermes-webui#2427 by [@&#8203;franksong2702](https://github.com/franksong2702) (fixes [#&#8203;2423](nesquena/hermes-webui#2423)) — Recover already-journaled visible assistant text and tool cards when a WebUI process restart interrupts an in-flight browser-originated turn. The stale-stream repair path now materializes run-journal output before the explicit interrupted marker instead of collapsing the turn to "no agent output was recovered."

##### Documentation

- **PR [#&#8203;2416](nesquena/hermes-webui#2416 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (refs [#&#8203;1925](nesquena/hermes-webui#1925)) — Expand the runtime-adapter RFC with the concrete Slice 2 adapter-seam contract: minimal `RuntimeAdapter` methods, payload fields, `legacy-direct` / `legacy-journal` feature-flag rollback path, legacy-backend mapping, explicit non-goals, and adapter-seam acceptance tests. Keeps the next step scoped to a reversible protocol-translator boundary over the journaled legacy path, not a runner/sidecar or execution-ownership move.

### [`v0.51.80`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05180--2026-05-17--Release-BD-stage-373--2-PR-batch--provider-config-flag-filter--stale-compaction-greeting-heuristic)

[Compare Source](nesquena/hermes-webui@v0.51.79...v0.51.80)

##### Fixed

- **PR [#&#8203;2415](nesquena/hermes-webui#2415 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2399](nesquena/hermes-webui#2399)) — `providers.only_configured` and other scalar flags under the top-level `providers:` config mapping no longer appear as fake provider groups in the model picker. Provider detection now only seeds picker groups from known provider ids/aliases or dict-shaped provider configs, so filtering flags cannot render as `Only-Configured`. The gating contract is documented inline in `api/config.py` (within the existing `_PROVIDER_MODELS`/`_PROVIDER_DISPLAY` membership block) so the test\_issue604 source-scan stays satisfied.
- **PR [#&#8203;2417](nesquena/hermes-webui#2417 by [@&#8203;nesquena-hermes](https://github.com/nesquena-hermes) (co-authored by [@&#8203;franksong2702](https://github.com/franksong2702), supersedes [#&#8203;2309](nesquena/hermes-webui#2309), closes [#&#8203;2308](nesquena/hermes-webui#2308)) — Compressed sessions with hidden "resume active task" context no longer treat a short fresh greeting (`hi`, `hello`, plus 6 CJK greetings) as implicit permission to continue an old agent task. Explicit continuation prompts (`continue`, `resume`, plus 4 CJK continuation phrases) still keep the compacted task context. The new helpers (`_normalize_fresh_chat_text`, `_is_casual_fresh_chat_message`, `_has_task_resume_compaction_marker`, `_context_messages_for_new_turn`) require BOTH the compaction phrase AND a task-resume keyword in the SAME message before treating it as a stale-task marker (precision-preserving guard). Length cap of 24 chars + workspace-prefix normalization + exact greeting-set match prevent false positives. CJK greetings/continuation terms are stored as Python `\u`-escape sequences so `api/streaming.py` passes the `test_title_sanitization::test_title_generation_source_has_no_cjk_literals` English-only-source invariant; runtime values are unchanged. Stage-372 Opus advisor pass caught two CJK codepoint typos (`嘖→嗨`, `哈喂→哈喽`) in the maintainer rebase and corrected them; new regression test `test_all_cjk_greetings_drop_stale_compaction_context` pins all 6 CJK greetings against future codepoint drift with `U+XXXX` failure messages.

### [`v0.51.79`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05179--2026-05-16--Release-BC-stage-372--5-PR-batch--text-mode-image-history-fix--Activity-group-compression-boundary--named-custom-provider-routing--quota-chip-Settings-toggle--RFC-docs)

[Compare Source](nesquena/hermes-webui@v0.51.78...v0.51.79)

##### Added

- **PR [#&#8203;2413](nesquena/hermes-webui#2413 (self-built follow-up to v0.51.78's [#&#8203;2082](nesquena/hermes-webui#2082), closes the quota-chip default-on regression) — New "Show provider quota chip in composer" checkbox in Settings → Preferences, default off. When disabled (the new default), the chip is hidden at all viewports and the `/api/provider/quota` fetch is skipped entirely. When enabled, the existing `@media (max-width:1399.98px)` gate from stage-371 still restricts the chip to wide desktops only. Per Nathan's directive 2026-05-16 immediately after stage-371 shipped — users get explicit agency over an ambient composer-chrome element. Wired through `api/config.py` `_SETTINGS_DEFAULTS`, `static/boot.js`, `static/panels.js` round-trip, `static/ui.js` short-circuit-when-disabled, `static/index.html` Settings field, and 11 locales in `static/i18n.js`.

##### Fixed

- **PR [#&#8203;2406](nesquena/hermes-webui#2406 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2398](nesquena/hermes-webui#2398)) — The fallback synchronous `POST /api/chat` route now passes the active WebUI config into the conversation-history sanitizer, so text-mode providers do not receive historical native `image_url` content parts when direct API callers use the legacy chat endpoint. This brings the sync route in line with the streaming chat path fixed for [#&#8203;2297](nesquena/hermes-webui#2297).
- **PR [#&#8203;2408](nesquena/hermes-webui#2408 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2404](nesquena/hermes-webui#2404)) — Auto-compression cards now close the current live Activity burst before rendering, so post-compression tools start a fresh `Activity` row instead of joining the pre-compression tool group across a real timeline/context boundary. Adds a `closeCurrentLiveActivityGroup()` helper that clears the `data-live-activity-current` marker before `appendLiveCompressionCard()` inserts the compression card. Resolves the DEFER from stage-370 Opus advisor review of PR [#&#8203;2390](nesquena/hermes-webui#2390).
- **PR [#&#8203;2411](nesquena/hermes-webui#2411 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (fixes [#&#8203;2405](nesquena/hermes-webui#2405)) — Named `custom:*` providers no longer lose vendor-prefixed model selections when the static model picker has not hydrated that model yet. The frontend now treats named custom providers as routable aggregators for both mismatch-warning suppression and missing-dropdown fallback, and live-fetched models keep explicit `@custom:name:` provider context so selections persist instead of snapping back to the configured default.

##### Documentation

- **PR [#&#8203;2407](nesquena/hermes-webui#2407 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) — Document the [#&#8203;1925](nesquena/hermes-webui#1925) runtime-adapter gate update: Slice 1 run-journal replay has now passed a 100-trial synthetic replay/restart validation pass on current `origin/master`, [#&#8203;2313](nesquena/hermes-webui#2313 selected-session chat SSE cap is shipped, and Slice 2 is ready for a reversible adapter-seam planning PR without moving execution ownership yet.

##### Test infrastructure

- New regression test `tests/test_quota_chip_settings_toggle.py` (6 cases) pins the quota-chip toggle invariants: Settings field present with i18n labels, `show_quota_chip` default-`False` in `_SETTINGS_DEFAULTS` + `_SETTINGS_BOOL_KEYS`, render/refresh both short-circuit when disabled (no wasted API calls), boot initializes `window._showQuotaChip` from settings + default-false on settings-fetch failure, full panels.js round-trip, 11 locale strings present.

### [`v0.51.78`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05178--2026-05-16--Release-BB-stage-371--stuck-PR-sweep-salvage--RTL-chat--ambient-quota-chip-with-composer-clutter-gate)

[Compare Source](nesquena/hermes-webui@v0.51.77...v0.51.78)

##### Added

- **PR [#&#8203;2409](nesquena/hermes-webui#2409 (maintainer follow-up from 2026-05-16 stuck-PR sweep, co-authored by [@&#8203;malulian](https://github.com/malulian) and [@&#8203;ai-ag2026](https://github.com/ai-ag2026), closes [#&#8203;1721](nesquena/hermes-webui#1721) and [#&#8203;2082](nesquena/hermes-webui#2082)) — Two stalled contributor PRs absorbed into one self-built release after Telegram UX approval across mobile/laptop/desktop/wide viewports.
  - **Right-to-left chat layout (salvaged from [#&#8203;1721](nesquena/hermes-webui#1721) by [@&#8203;malulian](https://github.com/malulian))** — New Settings → Preferences toggle, default off, flips the chat-area direction for Arabic and Hebrew users. Honors [@&#8203;aronprins](https://github.com/aronprins)' design review on PR [#&#8203;1721](nesquena/hermes-webui#1721) (May 13 2026): drops the contributor's composer footer toggle button to keep composer real estate clean. Implementation includes a flash-prevention bootstrap `<script>` in `<head>` (applies `chat-content-rtl` class synchronously before any chat content paints), scoped CSS that only flips `.msg-row`, `.msg-body` tables, `.tool-call-group-summary`, and the composer `textarea#msg` — the sidebar, workspace panel, settings panel, and any other UI element stay left-to-right. Code blocks (`pre`, `code`, `kbd`, `samp`, `tt`, `.hljs`, `.code-block`) and tool-call group bodies force `direction:ltr; text-align:left; unicode-bidi:isolate` even under RTL, because Arabic and Hebrew developers still write English code, command lines, and JSON the same way English developers do (visually verified with embedded Python in an Arabic SSE conversation). Localized in 11 locales (en, it, ja, ru, es, de, zh-CN, zh-TW, pt, ko, fr).
  - **Ambient provider quota chip (overridden from [#&#8203;2082](nesquena/hermes-webui#2082) by [@&#8203;ai-ag2026](https://github.com/ai-ag2026))** — New green pill chip in the composer footer that surfaces the active provider's remaining quota (OpenRouter credit balance shaped as `$X.YZ`, or account-limit-shaped providers as `N%`), with click-through to Settings → Providers. Fetches `/api/provider/quota` on boot and on tab visibility return. Hidden below 1400px viewport via `@media (max-width:1400px) { display:none !important }` because the composer footer at 1280px laptop and 1440px standard desktop was already tight and the chip squeezed adjacent chips (model picker truncated from `Claude Sonnet 4 7` to `Claude Sonnet 4`, workspace dropdown lost text). Mobile users find quota through the dedicated mobile-config drawer; laptop users follow the chip's click-target into Settings → Providers anyway. The chip's value proposition (ambient quota visibility) is preserved on wide displays where there's genuine composer room without trading off existing chip readability.

##### Test infrastructure

- New regression test `tests/test_pr1721_rtl_salvage.py` (8 cases) pins the RTL salvage invariants: Settings field + i18n keys present, no composer footer button (negative assertion encoding [@&#8203;aronprins](https://github.com/aronprins)' design objection), bootstrap script runs synchronously in `<head>` before paint, CSS scoped to chat only (negative tests against `.sidebar`, `.settings-panel`, `.workspace-panel`, `html`, `body` rules), code blocks force LTR under RTL, tool-call bodies force LTR under RTL, panels.js load/save round-trip, `rtl` in `api/config.py` DEFAULTS and writable-key allow-list, 11 locale strings present.

### [`v0.51.77`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05177--2026-05-16--Release-BA-stage-370--1-PR-follow-up--live-Activity-grouping-boundary-fix)

[Compare Source](nesquena/hermes-webui@v0.51.76...v0.51.77)

##### Fixed

- **PR [#&#8203;2390](nesquena/hermes-webui#2390 by [@&#8203;franksong2702](https://github.com/franksong2702) (refs [#&#8203;2376](nesquena/hermes-webui#2376), [#&#8203;2344](nesquena/hermes-webui#2344), [#&#8203;2347](nesquena/hermes-webui#2347), [#&#8203;2377](nesquena/hermes-webui#2377)) — Live progress Activity grouping no longer degrades consecutive tool calls into repeated `Activity: 1 tool` rows. The frontend was using one reset helper for two different jobs — resetting where the next assistant text segment should render, and closing the current live Activity group — but those are not the same operation. Tool starts now only reset the next-text-segment anchor; the live Activity group closes only when the model emits a visible `interim_assistant` progress update (the actual timeline boundary). The flow stays:

  ```text
  Thinking card
  visible progress note
  Activity: N related tools
  visible progress note
  Activity: N related tools
  final answer
  ```

  Adds a WebUI-only ephemeral progress contract in `api/streaming.py` that asks multi-step tool-heavy turns to emit concise visible progress notes in the user's language, while explicitly forbidding exposure of hidden reasoning, chain-of-thought, scratchpads, secrets, raw logs, or long tool output. Any selected personality prompt is preserved. New regressions cover the progress-contract reach-through, the interim-assistant split boundary, and the consecutive-tools-in-one-Activity-row invariant.

### [`v0.51.76`](https://github.com/nesquena/hermes-webui/blob/HEAD/CHANGELOG.md#v05176--2026-05-16--Release-AZ-stage-369--4-PR-safe-lane-batch--live-timeline-preservation--OpenRouter-cost-history--chat-stream-cap--credential-pool-cache)

[Compare Source](nesquena/hermes-webui@v0.51.75...v0.51.76)

##### Added

- **PR [#&#8203;2195](nesquena/hermes-webui#2195 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (refs [#&#8203;692](nesquena/hermes-webui#692)) — OpenRouter cost history backend. New `GET /api/providers/openrouter/cost_history` endpoint backed by daily snapshots from OpenRouter's `/auth/key` cumulative spend. Process-local lock around the snapshot read-modify-write critical section so concurrent dashboard refreshes or multiple tabs cannot overwrite newer reads with stale ones. Delta computation handles cumulative-counter resets (key rotation, OpenRouter-side reset) by starting a fresh series and using the current value as that day's delta rather than emitting negative spend. Backend-only slice; the 7-day daily cost chart UI is a separate follow-up.

##### Fixed

- **PR [#&#8203;2347](nesquena/hermes-webui#2347 by [@&#8203;franksong2702](https://github.com/franksong2702) (fixes [#&#8203;2344](nesquena/hermes-webui#2344)) — Preserve live agent timeline across session switches. Previously, switching away from an active stream and returning rebuilt the turn from the persisted `INFLIGHT` tail, which is enough to reconnect the stream but is not a full-fidelity DOM timeline — Thinking/tool grouping flattened, interim assistant text moved away from its surrounding context, auto-compression cards could project twice. The restore path now snapshots the live assistant turn DOM during the active stream and, on return, loads the persisted transcript first then merges the live snapshot back in so the on-screen scene is preserved as the user left it. Stamping `row.dataset.sessionId` at turn creation prevents the new live-turn sites from re-triggering the lossy rebuild path.

- **PR [#&#8203;2393](nesquena/hermes-webui#2393 by [@&#8203;Michaelyklam](https://github.com/Michaelyklam) (refs [#&#8203;2313](nesquena/hermes-webui#2313)) — Cap live chat stream transports to the selected conversation. Previously, keeping many sessions open accumulated one long-lived `/api/chat/stream` EventSource per session. New `closeOtherLiveStreams(activeSid)` helper in `static/messages.js`; `attachLiveStream()` now reuses an existing same-session transport first, closes other sessions' chat SSE transports, then opens or replaces the selected session's stream. Background sessions still reattach normally when the user selects them — only the SSE transport is pruned, not the server-side stream ownership. New regression test pins the ordering (reuse first, prune background streams next, replace active transport last).

- **PR [#&#8203;2396](nesquena/hermes-webui#2396 by [@&#8203;starship-s](https://github.com/starship-s) — Preserve session agents for credential pools. The per-session `AIAgent` cache signature previously mixed stable agent identity with the volatile resolved API key, so credential-pool providers (where each request can resolve a different runtime token even when provider/model config is unchanged) missed the cache every turn and rebuilt the agent — losing warmed cross-turn state such as memory-provider prefetch results for providers like Hindsight. New credential-aware cache-signature helper uses a stable sentinel for credential-pool routes while preserving hashed API-key identity for non-pool routes; reused cached agents refresh runtime credentials in place; `AIAgent._primary_runtime` stays aligned after refresh so fallback/transport recovery cannot resurrect an old token; agents still in fallback-active state rebuild rather than mutate to avoid mixed primary/fallback runtime state. Static non-pool API keys still participate in the cache signature so explicit credential changes continue to invalidate.

</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/528
SysAdminDoc pushed a commit to SysAdminDoc/hermes-webui that referenced this pull request Jun 26, 2026
bernyforce pushed a commit to bernyforce/hermes-webui that referenced this pull request Jul 29, 2026
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