From 95933808995329bcd873581a39861e28661b1b30 Mon Sep 17 00:00:00 2001 From: BonyFish Date: Sat, 16 May 2026 13:09:09 +0800 Subject: [PATCH 1/7] fix: add i18n support to settings sidebar menu items Adds data-i18n attributes to all settings sidebar menu items (Conversation, Appearance, Preferences, Plugins, System) so they respect the user's selected locale. Also adds missing settings_tab_plugins key to English locale. --- static/i18n.js | 1 + static/index.html | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/static/i18n.js b/static/i18n.js index 0e95decf..99fa4793 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -508,6 +508,7 @@ const LOCALES = { settings_tab_conversation: 'Conversation', settings_tab_appearance: 'Appearance', settings_tab_preferences: 'Preferences', + settings_tab_plugins: 'Plugins', settings_tab_system: 'System', settings_title: 'Settings', settings_save_btn: 'Save Settings', diff --git a/static/index.html b/static/index.html index af1c017e..884ca786 100644 --- a/static/index.html +++ b/static/index.html @@ -279,15 +279,15 @@
From ddf8eb5d54ad47f64c74a12b4a0f3e7d7881280d Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 16 May 2026 05:42:53 +0000 Subject: [PATCH 2/7] stage-367: locale-parity fix for settings_tab_plugins (10 locales) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #2367 added settings_tab_plugins to English only. The locale-parity tests (test_chinese_locale.py, test_japanese_locale.py, etc.) require every English key to exist in all 10 other locales. CI failed on 5 of them. Adds the key to all 10 non-English locales with translations: - it: Plugin, ja: プラグイン, ru: Плагины, es/de/pt/fr: Plugins (loanword), zh: 插件, zh-TW: 外掛, ko: 플러그인 Co-authored-by: mccxj --- static/i18n.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/static/i18n.js b/static/i18n.js index 99fa4793..30c497f1 100644 --- a/static/i18n.js +++ b/static/i18n.js @@ -1698,6 +1698,7 @@ const LOCALES = { settings_tab_conversation: 'Conversazione', settings_tab_appearance: 'Aspetto', settings_tab_preferences: 'Preferenze', + settings_tab_plugins: 'Plugin', settings_tab_system: 'Sistema', settings_title: 'Impostazioni', settings_save_btn: 'Salva Impostazioni', @@ -2879,6 +2880,7 @@ const LOCALES = { settings_tab_conversation: '会話', settings_tab_appearance: '外観', settings_tab_preferences: '環境設定', + settings_tab_plugins: 'プラグイン', settings_tab_system: 'システム', settings_title: '設定', settings_save_btn: '設定を保存', @@ -4565,6 +4567,7 @@ const LOCALES = { settings_tab_appearance: 'Appearance', settings_tab_conversation: 'Conversation', settings_tab_preferences: 'Preferences', + settings_tab_plugins: 'Плагины', settings_tab_system: 'System', status_updated: 'Updated', status_ephemeral: 'Ephemeral snapshot — not saved to transcript history.', @@ -5673,6 +5676,7 @@ const LOCALES = { settings_tab_appearance: 'Appearance', settings_tab_conversation: 'Conversation', settings_tab_preferences: 'Preferences', + settings_tab_plugins: 'Plugins', settings_tab_system: 'System', status_updated: 'Updated', status_ephemeral: 'Ephemeral snapshot — not saved to transcript history.', @@ -6517,6 +6521,7 @@ const LOCALES = { settings_tab_appearance: 'Appearance', settings_tab_conversation: 'Conversation', settings_tab_preferences: 'Preferences', + settings_tab_plugins: 'Plugins', settings_tab_system: 'System', status_updated: 'Updated', status_ephemeral: 'Ephemeral snapshot — not saved to transcript history.', @@ -7910,6 +7915,7 @@ const LOCALES = { settings_tab_appearance: '外观', settings_tab_conversation: '对话', settings_tab_preferences: '偏好', + settings_tab_plugins: '插件', settings_tab_system: '系统', status_updated: '已更新', status_ephemeral: '临时快照 — 不会保存到对话记录。', @@ -8369,6 +8375,7 @@ const LOCALES = { settings_tab_conversation: '對話', settings_tab_appearance: '外觀', settings_tab_preferences: '偏好設定', + settings_tab_plugins: '外掛', settings_tab_system: '系統', settings_title: '\u8a2d\u5b9a', settings_save_btn: '\u5132\u5b58\u8a2d\u5b9a', @@ -9638,6 +9645,7 @@ const LOCALES = { settings_tab_conversation: 'Conversa', settings_tab_appearance: 'Aparência', settings_tab_preferences: 'Preferências', + settings_tab_plugins: 'Plugins', settings_tab_system: 'Sistema', settings_title: 'Configurações', settings_save_btn: 'Salvar Configurações', @@ -10722,6 +10730,7 @@ const LOCALES = { settings_tab_conversation: '대화', settings_tab_appearance: '외형', settings_tab_preferences: '환경설정', + settings_tab_plugins: '플러그인', settings_tab_system: '시스템', settings_title: '설정', settings_save_btn: '설정 저장', @@ -11823,6 +11832,7 @@ const LOCALES = { settings_tab_conversation: 'Conversation', settings_tab_appearance: 'Apparence', settings_tab_preferences: 'Préférences', + settings_tab_plugins: 'Plugins', settings_tab_system: 'Système', settings_title: 'Paramètres', settings_save_btn: 'Enregistrer les paramètres', From 80be1d08dc7e727a72472fee476162856ce865b5 Mon Sep 17 00:00:00 2001 From: Frank Song Date: Sat, 16 May 2026 12:30:15 +0800 Subject: [PATCH 3/7] Fix Markdown table cell paragraph spacing --- static/style.css | 1 + tests/test_markdown_table_cell_spacing.py | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 tests/test_markdown_table_cell_spacing.py diff --git a/static/style.css b/static/style.css index cdcd98b9..257665e0 100644 --- a/static/style.css +++ b/static/style.css @@ -906,6 +906,7 @@ .role-icon.assistant{background:var(--accent-bg-strong);color:var(--accent-text);border:1px solid var(--accent-bg-strong);} .msg-body{font-size:14px;line-height:1.75;color:var(--text);padding-left:30px;max-width:680px;overflow-wrap:anywhere;} .msg-body p{margin-bottom:10px;}.msg-body p:last-child{margin-bottom:0;} + .msg-body td p,.msg-body th p{margin:0;} .msg-body ul,.msg-body ol{margin:6px 0 10px 20px;}.msg-body li{margin-bottom:3px;} .msg-body h1,.msg-body h2,.msg-body h3,.msg-body h4,.msg-body h5,.msg-body h6{font-weight:700;color:var(--strong,var(--text));line-height:1.3;} .msg-body h1{font-size:24px;margin:24px 0 12px;border-bottom:1px solid var(--border);padding-bottom:6px;} diff --git a/tests/test_markdown_table_cell_spacing.py b/tests/test_markdown_table_cell_spacing.py new file mode 100644 index 00000000..faa4bba1 --- /dev/null +++ b/tests/test_markdown_table_cell_spacing.py @@ -0,0 +1,20 @@ +"""Regression tests for Markdown table cell spacing.""" +from pathlib import Path + + +REPO_ROOT = Path(__file__).resolve().parents[1] +STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8") + + +def test_table_cell_paragraph_margins_are_reset(): + """Paragraphs inserted inside Markdown table cells should not add extra row height.""" + assert ".msg-body td p,.msg-body th p{margin:0;}" in STYLE_CSS + + +def test_table_cell_paragraph_reset_follows_global_message_paragraph_rule(): + """The table-specific reset must override the generic message paragraph spacing rule.""" + generic_rule = ".msg-body p{margin-bottom:10px;}" + table_reset = ".msg-body td p,.msg-body th p{margin:0;}" + + assert generic_rule in STYLE_CSS + assert STYLE_CSS.index(generic_rule) < STYLE_CSS.index(table_reset) From 58a43d7abd2a76c8f45b87e5f6d111fee4cd9904 Mon Sep 17 00:00:00 2001 From: Frank Song Date: Sat, 16 May 2026 12:32:18 +0800 Subject: [PATCH 4/7] Document WebUI run state consistency contract --- docs/rfcs/README.md | 4 + .../webui-run-state-consistency-contract.md | 150 ++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 docs/rfcs/webui-run-state-consistency-contract.md diff --git a/docs/rfcs/README.md b/docs/rfcs/README.md index 78fed41c..58b9291c 100644 --- a/docs/rfcs/README.md +++ b/docs/rfcs/README.md @@ -42,5 +42,9 @@ First-time contributor RFCs should be discussed in an issue before opening a PR. event/control contract, runtime-state ownership matrix, acceptance catalog, and reversible migration gates for moving WebUI execution behind an explicit adapter boundary. +- [`webui-run-state-consistency-contract.md`](webui-run-state-consistency-contract.md) + — #2361 consistency rules for keeping transcript, model context, live streams, + replay, compression, and session metadata coherent during active and recovered + WebUI runs. - [`turn-journal.md`](turn-journal.md) — Crash-safe WebUI turn journal for recovering interrupted chat submissions. diff --git a/docs/rfcs/webui-run-state-consistency-contract.md b/docs/rfcs/webui-run-state-consistency-contract.md new file mode 100644 index 00000000..96d9ff3b --- /dev/null +++ b/docs/rfcs/webui-run-state-consistency-contract.md @@ -0,0 +1,150 @@ +# WebUI Run State Consistency Contract + +- **Status:** Proposed +- **Author:** @franksong2702 +- **Created:** 2026-05-16 +- **Tracking issue:** [#2361](https://github.com/nesquena/hermes-webui/issues/2361) +- **Related architecture:** [#1925](https://github.com/nesquena/hermes-webui/issues/1925), [`hermes-run-adapter-contract.md`](hermes-run-adapter-contract.md) + +## Problem + +A single WebUI agent turn is represented by several overlapping state layers: + +- the visible transcript the user can read, +- the model context / `context_messages` the agent actually receives, +- `pending_user_message` and active stream metadata, +- live SSE events and in-memory stream state, +- durable run journal / replay state, +- automatic compression summaries and active-task handoff text, +- the browser's live timeline DOM/cache, +- sidebar ordering, unread state, and `updated_at` metadata. + +Those layers are not independent. When they drift apart, the user sees failures +that look unrelated: a prompt is visible but missing from recovered model +context, a live run loses or reorders thinking/tool cards after switching +sessions, cleanup makes old sessions look newly active, replay duplicates content, +or automatic compression reference material appears inside the active turn. + +This RFC defines a consistency contract for those layers. It complements the +larger run adapter direction in #1925 by documenting what must remain coherent +while WebUI still has multiple overlapping state stores. + +## Goals + +- Define the state layers involved in active and recovered WebUI turns. +- Make the source-of-truth expectations explicit for each layer. +- Give reviewers a checklist for streaming, replay, compression, recovery, + model-context, and sidebar changes. +- Map recent real issues to reusable invariants so future fixes do not solve the + same class of bug one symptom at a time. + +## Non-goals + +- Do not implement a runner process, sidecar, or new runtime boundary here. +- Do not replace #1925 or the run adapter contract. +- Do not rewrite the streaming protocol in this RFC. +- Do not reopen already-fixed narrow bugs. +- Do not make this a catch-all for unrelated UI polish. + +## State Layers + +| Layer | Purpose | Source-of-truth expectation | Must not do | +|---|---|---|---| +| Visible transcript | Shows what the user and assistant said | Session transcript plus live replay should produce one chronological user-visible story | Hide the user turn that started active work, or show internal recovery text as current user intent | +| Model context / `context_messages` | Supplies conversation state to the agent | Must include the current visible user turn unless deliberately excluded with a user-visible reason | Let the agent resume from context that contradicts what the user can see | +| Pending turn metadata | Bridges submitted-but-not-yet-finalized user input | Must identify the user turn and stream that own active work | Become a permanent duplicate transcript row after recovery | +| Live stream / SSE | Delivers active runtime events to the browser | Must remain an observation path, not the only durable truth for already-emitted events | Lose the visible scene on refresh, reconnect, or session switch | +| Run journal / replay | Rebuilds emitted runtime events after reconnect or restart | Must be cursor-safe and idempotent | Duplicate assistant text, thinking text, tool cards, or compression cards | +| Compression summary / handoff | Gives the agent recovery context after automatic compression | Must remain agent-facing recovery material unless explicitly rendered as history | Pollute the active turn or become implicit current user intent | +| Live UI scene/cache | Preserves expanded rows, in-progress cards, local scroll, and transient grouping | May optimize presentation but must be rebuildable or degradable from transcript/replay | Become the only place where chronological ordering exists | +| Sidebar/session metadata | Helps the user find active and recent sessions | Must reflect meaningful user or assistant activity | Treat background cleanup as a fresh user-facing update | + +## Core Invariants + +1. **Visible current turns enter model context.** If the user can see a current + prompt and WebUI asks the model to continue that work, the prompt must be in + the reconstructed model context unless WebUI shows an explicit reason it was + excluded. +2. **Active turn UI keeps its owner.** The user turn that started active work + must remain visible before assistant text, thinking cards, tool cards, or + activity groups that belong to that work. +3. **Reattach preserves order or degrades clearly.** Refresh, reconnect, and + session switch must preserve chronological live-scene order. If WebUI cannot + restore the exact live scene, it should downgrade to an explicit structured + replay state instead of silently reordering content. +4. **Maintenance is not activity.** Runtime maintenance such as stale-stream + cleanup, orphan repair, or background compression must not refresh sidebar + ordering, unread markers, or active-session affordances as if the user or + assistant just acted. +5. **Replay is idempotent.** Replaying a run from a cursor must not duplicate + transcript rows, thinking content, interim assistant text, tool cards, or + compression cards. +6. **Compression is not current intent.** Automatic compression summaries and + reference cards are recovery/handoff material. They must not be treated as a + new user request, active-turn content, or the default visible explanation for + the current answer. +7. **Observation has a degraded path.** Long-running or many-session observation + should expose enough heartbeat/degraded status that the UI does not appear + silent and ordinary APIs do not stall behind active streams. +8. **Every mutation names its layer.** A PR touching streaming, recovery, + context reconstruction, compression, replay, or sidebar metadata should state + which layer it changes and what regression proves the invariant still holds. + +## Review Checklist + +Use this checklist for PRs that touch run state, streaming, replay, compression, +context reconstruction, or session metadata: + +- Which state layers does this PR read or write? +- Which layer is the source of truth after this change? +- Can the visible transcript and model context diverge? If yes, is that + deliberate and user-visible? +- What happens after browser refresh, session switch, SSE reconnect, and WebUI + restart? +- Does replay rebuild the same scene without duplicates? +- Can this change move a session in the sidebar without meaningful user or + assistant activity? +- Can automatic compression or recovery text become visible active-turn content? +- What test or manual evidence proves the invariant? + +## Existing Issue Map + +| Example | State boundary exposed | Relevant invariant | +|---|---|---| +| [#2341](https://github.com/nesquena/hermes-webui/issues/2341) / [#2342](https://github.com/nesquena/hermes-webui/pull/2342) | Active reattach could show agent activity without the pending user turn that started it | 2 | +| [#2344](https://github.com/nesquena/hermes-webui/issues/2344) / [#2347](https://github.com/nesquena/hermes-webui/pull/2347) | Session switching could lose or reorder the live thinking/tool/interim timeline | 3, 5 | +| [#2345](https://github.com/nesquena/hermes-webui/issues/2345) / [#2349](https://github.com/nesquena/hermes-webui/pull/2349) | Stale stream cleanup could mutate `updated_at` and resurface old sessions | 4 | +| [#2346](https://github.com/nesquena/hermes-webui/issues/2346) / [#2348](https://github.com/nesquena/hermes-webui/pull/2348) | Thinking cards could repeat interim assistant progress text | 5 | +| [#2353](https://github.com/nesquena/hermes-webui/issues/2353) / [#2354](https://github.com/nesquena/hermes-webui/pull/2354) | Recovered pending user turns could be visible but missing from model context | 1 | +| [#2355](https://github.com/nesquena/hermes-webui/issues/2355) / [#2357](https://github.com/nesquena/hermes-webui/pull/2357) | Auto-compression rotation could leave reference-only cards in the active conversation tail | 3, 6 | +| [#2308](https://github.com/nesquena/hermes-webui/issues/2308) / [#2309](https://github.com/nesquena/hermes-webui/pull/2309) | Compressed sessions could resume stale agent tasks when the user starts an ordinary fresh chat | 6 | +| [#2283](https://github.com/nesquena/hermes-webui/pull/2283) | Run event journal replay provides the foundation for ordered recovery | 5 | + +These references are evidence for the contract. This RFC does not make the +linked implementation PRs dependent on this document, and it does not close the +tracking issue by itself. + +## Relationship To The Run Adapter RFC + +The run adapter RFC defines the longer-term event/control boundary for WebUI and +Hermes runtime ownership. This RFC defines the consistency rules that the current +WebUI and any future adapter-backed implementation must preserve. + +The two documents should be read together: + +- The adapter contract answers: "Where should execution ownership live?" +- This consistency contract answers: "How do transcript, context, streams, + replay, compression, and UI metadata stay coherent while execution is active + or being recovered?" + +## Rollout Plan + +1. Land this RFC as a reviewable draft and refine it through PR discussion. +2. Link future streaming/recovery/compression/sidebar PRs back to the invariant + they intentionally preserve or change. +3. Convert recurring checklist items into focused regression tests where + practical. +4. If #1925 introduces a new adapter-backed runtime layer, update this RFC or + replace it with the accepted implementation contract so these invariants do + not live only in historical discussion. + From f82a763dfba65978d79c93002af72fc4e4be49c5 Mon Sep 17 00:00:00 2001 From: BonyFish Date: Sat, 16 May 2026 12:38:39 +0800 Subject: [PATCH 5/7] fix: support list format for custom_providers.models in model dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The get_available_models() function only handled dict-format models (`{model_id: {}}`) for custom_providers entries, silently dropping models specified as YAML lists (`[model1, model2]`) or list of dicts (`[{id: ..., label: ...}]`). This caused users who define their custom providers with list-format model declarations to see zero or incomplete model entries in both Settings → Preferences → Default Model dropdown and the chat interface model picker. The fix adds an `elif isinstance(_cp_models_dict, list)` branch with support for three list sub-formats: - Plain string list: `models: [m1, m2]` - Dict list: `models: [{id: m1, label: ...}]` - Mixed: `models: [m1, {id: m2}]` Refs: hermes-agent issue where YAML list models were invisible --- api/config.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/api/config.py b/api/config.py index 77b601f6..5ab5043f 100644 --- a/api/config.py +++ b/api/config.py @@ -3194,6 +3194,16 @@ def get_available_models() -> dict: for _m_id in _cp_models_dict: if isinstance(_m_id, str) and _m_id.strip() and _m_id not in _cp_model_ids: _cp_model_ids.append(_m_id.strip()) + elif isinstance(_cp_models_dict, list): + for _item in _cp_models_dict: + if isinstance(_item, str): + _mid = _item.strip() + if _mid and _mid not in _cp_model_ids: + _cp_model_ids.append(_mid) + elif isinstance(_item, dict): + _mid = str(_item.get("id") or _item.get("model") or _item.get("name") or "").strip() + if _mid and _mid not in _cp_model_ids: + _cp_model_ids.append(_mid) for _cp_model in _cp_model_ids: _dedup_key = f"{_slug}:{_cp_model}" if _slug else _cp_model From b48e44a24a4eb2837cdc5fe7ae173b0a7f06d40d Mon Sep 17 00:00:00 2001 From: BonyFish Date: Sat, 16 May 2026 12:44:25 +0800 Subject: [PATCH 6/7] Add tests for list-format custom_providers.models Covers 9 scenarios: plain string list, dict list, unnamed provider, mixed formats, dedup, empty list, fallback keys, @-prefix routing, and mixed list/dict items from multiple providers. --- .../test_issue1106_custom_providers_models.py | 158 ++++++++++++++++++ 1 file changed, 158 insertions(+) diff --git a/tests/test_issue1106_custom_providers_models.py b/tests/test_issue1106_custom_providers_models.py index 2be22e88..9b0fd16c 100644 --- a/tests/test_issue1106_custom_providers_models.py +++ b/tests/test_issue1106_custom_providers_models.py @@ -241,3 +241,161 @@ class TestCustomProvidersModelsDict: ids = [m["id"] for m in group["models"]] assert "@custom:sub2api:gpt-5.4-mini" in ids assert "@custom:sub2api:gpt-5.4" in ids + + +class TestCustomProvidersModelsList: + """custom_providers entries with a 'models' list should also populate the dropdown.""" + + def test_models_list_of_strings_appear_in_dropdown(self): + """Each entry in custom_providers[].models list of strings should appear as a selectable model.""" + result = _models_with_cfg( + model_cfg={"provider": "custom"}, + custom_providers=[ + { + "name": "MultiModel", + "base_url": "http://multi:8080/v1", + "model": "base-v1", + "models": ["model-a", "model-b", "model-c"], + } + ], + ) + ids = _all_model_ids_bare(result) + for expected in ["base-v1", "model-a", "model-b", "model-c"]: + assert expected in ids, f"Expected '{expected}' in model IDs, got {ids}" + + def test_models_list_of_dicts_with_id_appear_in_dropdown(self): + """Each dict entry in models list with 'id' key should appear.""" + result = _models_with_cfg( + model_cfg={"provider": "custom"}, + custom_providers=[ + { + "name": "ApiHub", + "models": [ + {"id": "gpt-5", "label": "GPT-5"}, + {"id": "claude-4", "label": "Claude Opus 4"}, + ], + } + ], + ) + ids = _all_model_ids_bare(result) + assert "gpt-5" in ids + assert "claude-4" in ids + + def test_list_of_dicts_falls_back_to_model_name_when_no_id(self): + """Dict entries in models list should fall back to 'model' or 'name' when 'id' is absent.""" + result = _models_with_cfg( + model_cfg={"provider": "custom"}, + custom_providers=[ + { + "name": "FlexAPI", + "models": [ + {"model": "via-model-key", "label": "From Model"}, + {"name": "via-name-key", "label": "From Name"}, + ], + } + ], + ) + ids = _all_model_ids_bare(result) + assert "via-model-key" in ids + assert "via-name-key" in ids + + def test_model_plus_list_dedup(self): + """When singular 'model' also appears in 'models' list, it should not be duplicated.""" + result = _models_with_cfg( + model_cfg={"provider": "custom"}, + custom_providers=[ + { + "name": "MyServer", + "model": "shared-model", + "models": ["shared-model", "unique-model"], + } + ], + ) + ids = _all_model_ids_bare(result) + assert ids.count("shared-model") == 1, f"'shared-model' should appear exactly once, got {ids.count('shared-model')}" + assert "unique-model" in ids + + def test_empty_models_list_is_ignored(self): + """An empty 'models' list should not break anything.""" + result = _models_with_cfg( + model_cfg={"provider": "custom"}, + custom_providers=[ + { + "name": "TestServer", + "model": "only-model", + "models": [], + } + ], + ) + ids = _all_model_ids_bare(result) + assert "only-model" in ids + + def test_unnamed_provider_models_list_works(self): + """custom_providers without 'name' and with a 'models' list should still populate 'Custom' group.""" + result = _models_with_cfg( + model_cfg={"provider": "custom"}, + custom_providers=[ + { + "models": ["anon-a", "anon-b"], + } + ], + ) + ids = _all_model_ids(result) + for expected in ["anon-a", "anon-b"]: + assert expected in ids, f"Expected '{expected}' in model IDs, got {ids}" + + def test_list_and_dict_providers_together(self): + """A mix of list-format and dict-format custom providers should all contribute models.""" + result = _models_with_cfg( + model_cfg={"provider": "custom"}, + custom_providers=[ + { + "name": "ListProv", + "models": ["list-m1", "list-m2"], + }, + { + "name": "DictProv", + "models": {"dict-m1": {}, "dict-m2": {}}, + }, + ], + ) + ids = _all_model_ids_bare(result) + for expected in ["list-m1", "list-m2", "dict-m1", "dict-m2"]: + assert expected in ids, f"Expected '{expected}' in model IDs, got {ids}" + + def test_mixed_list_items_string_and_dict(self): + """A single models list mixing strings and dicts should produce all entries.""" + result = _models_with_cfg( + model_cfg={"provider": "custom"}, + custom_providers=[ + { + "name": "MixedProv", + "models": [ + "plain-string", + {"id": "dict-id", "label": "Dict Label"}, + ], + } + ], + ) + ids = _all_model_ids_bare(result) + assert "plain-string" in ids + assert "dict-id" in ids + + def test_named_custom_list_models_prefixed_when_not_active_provider(self): + """List-format custom provider models must carry routing prefix when another provider is active.""" + result = _models_with_cfg( + model_cfg={"provider": "deepseek", "default": "deepseek-v4-pro"}, + custom_providers=[ + { + "name": "sub2api", + "base_url": "http://127.0.0.1:8080/v1", + "models": ["gpt-5.4-mini", "gpt-5.4"], + } + ], + ) + group = _group_for(result, "sub2api") + assert group is not None, "sub2api group missing" + assert group["provider_id"] == "custom:sub2api" + ids = [m["id"] for m in group["models"]] + assert "@custom:sub2api:gpt-5.4-mini" in ids, f"Expected @-prefixed ID, got {ids}" + assert "@custom:sub2api:gpt-5.4" in ids, f"Expected @-prefixed ID, got {ids}" From 8303d59acdd5ec760188a880d59ace2b5bc5d1d7 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Sat, 16 May 2026 05:45:01 +0000 Subject: [PATCH 7/7] stage-367: stamp CHANGELOG v0.51.74 (4-PR safe-lane batch) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v0.51.74 — Release AX: - PR #2362 (fixes #2360) — Markdown table cell paragraph spacing - PR #2363 (refs #2361 + #1925) — Run state consistency RFC docs - PR #2365 (fixes #1106) — custom_providers.models YAML list format - PR #2367 — Settings sidebar i18n (with maintainer locale-parity fix) None of the PRs touched CHANGELOG.md, so adding the canonical entries manually with proper PR # attribution and issue refs. Stage-367 maintainer fix on #2367: CI failed on 5 locale-parity tests because the PR added settings_tab_plugins to English only. Added translations to all 10 non-English locales (commit ddf8eb5d on the PR's integration branch). --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c2f8a81..6fa25bab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ ## [Unreleased] +## [v0.51.74] — 2026-05-16 — Release AX (stage-367 — 4-PR safe-lane batch — #2362 table-cell spacing + #2363 run-state-consistency RFC + #2365 custom_providers list-format + #2367 settings sidebar i18n) + +### Added + +- **PR #2363** by @franksong2702 (refs #2361, refs #1925) — Adds `docs/rfcs/webui-run-state-consistency-contract.md` as a documentation companion to the #1925 runtime-boundary RFC. Documents the shared coherence contract across visible transcript, model context, pending turn metadata, live stream, run journal, compression handoff, browser timeline cache, and sidebar metadata. Complementary to #1925: that RFC says where execution ownership should move, this one says what must stay coherent across the current and future state layers. + +### Fixed + +- **PR #2362** by @franksong2702 (fixes #2360) — Markdown table rows no longer become too tall when cell text is wrapped in paragraph tags by the renderer. Adds a table-specific CSS reset for `.msg-body td p` and `.msg-body th p` so the global `margin-bottom: 10px` rule on `.msg-body p` doesn't add unwanted vertical space inside table cells. Especially visible on narrow viewports such as iPad Safari/Chrome. + +- **PR #2365** by @mccxj (fixes #1106) — `get_available_models()` now handles YAML-list format `custom_providers.models` entries in addition to dict format. Pre-fix, declaring models as a list (`[m1, m2]`) or list-of-dicts (`[{id: m1, label: ...}]`) in `config.yaml` silently discarded every model from that provider in the picker dropdown because the code only recognized dict shape (`{model_id: {}}`). Now supports all three YAML shapes consistently with existing provider-config and live-models-fallback handlers. + +- **PR #2367** by @mccxj — Settings sidebar menu items (Conversation, Appearance, Preferences, Plugins, System) now respect locale selection. Pre-fix these were hardcoded English; only Providers had `data-i18n`. Adds `data-i18n` attributes plus the missing `settings_tab_plugins` key. **Stage-367 maintainer fix applied inline**: the PR only added the new key to English, breaking 5 locale-parity tests. Added `settings_tab_plugins` translations to all 10 non-English locales (it/ja/ru/es/de/zh/zh-TW/pt/ko/fr). + ## [v0.51.73] — 2026-05-16 — Release AW (stage-366 — 1-PR safe-lane batch — #2357 compression reference card anchoring fix) ### Fixed