From 0461b97d153860a115983c68e0a3eef71d372893 Mon Sep 17 00:00:00 2001 From: Frank Song Date: Thu, 14 May 2026 23:03:48 +0800 Subject: [PATCH] Escape model picker display text --- CHANGELOG.md | 2 ++ static/panels.js | 2 +- static/ui.js | 4 ++-- tests/test_model_picker_escaping.py | 20 ++++++++++++++++++++ 4 files changed, 25 insertions(+), 3 deletions(-) create mode 100644 tests/test_model_picker_escaping.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b6df1a29..eb962316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ### Fixed +- Model picker rows now escape provider-supplied model names and IDs before rendering, and the providers panel escapes load-error text before inserting it into the DOM. + - **PR #2234** by @Jordan-SkyLF (refines #2207) — Three update-banner improvements: (1) Update summaries no longer repeat the same bullet under both "What you'll notice" and "Worth knowing" — visible notice items keep priority, and the secondary section is omitted when there is no distinct detail to show. (2) Update summaries now cap large commit lists (24 + probe item) before sending them to the summarizer and disclose when the summary uses only the latest commit subjects, while keeping the full comparison link available — bounds summarizer cost on large update ranges while remaining honest about coverage. (3) Update banners now wrap generated-summary links and long update text on narrow/mobile screens inside the banner instead of pushing controls off-screen. 108-line regression coverage for short-target dedup, repeated Agent-summary bullets, large-range capped input, and responsive wrapping. - **PR #2236** by @jasonjcwu — Silent failure detection in `api/streaming.py` now scans only NEW messages, not the full conversation history. Pre-fix, the `_assistant_added` check at `_run_agent_streaming` scanned all messages in `result["messages"]` (including pre-turn history); if any prior turn contained an assistant response, `_assistant_added` was `True` and the apperror SSE event was silently skipped, leaving the user staring at a blank response after a provider 401/429/rate-limit error. Fix extracts a `_has_new_assistant_reply(all_messages, prev_count)` helper that only inspects messages beyond the pre-turn history offset (`_previous_context_messages`); applied to both the main detection path and the self-heal/retry `_heal_ok` check. 15-test regression suite covering empty/short/long-history scenarios, the heal path, and the `len < prev_count` edge-case fallback. Also includes a small alignment fix to `test_issue1857_usage_overwrite.py` so the FakeAgent message shape matches what the real agent produces. diff --git a/static/panels.js b/static/panels.js index 8ebf44d6..ca7e6733 100644 --- a/static/panels.js +++ b/static/panels.js @@ -5569,7 +5569,7 @@ async function loadProvidersPanel(){ list.appendChild(_buildProviderCard(p)); } }catch(e){ - list.innerHTML='
Failed to load providers: '+e.message+'
'; + list.innerHTML='
Failed to load providers: '+esc(e.message||String(e))+'
'; } } diff --git a/static/ui.js b/static/ui.js index 0c716cb8..f270909f 100644 --- a/static/ui.js +++ b/static/ui.js @@ -1206,7 +1206,7 @@ function renderModelDropdown(){ } } const badgeHtml=m.badge?`${esc(badgeLabel)}`:''; - row.innerHTML=`
${esc(modelName)}${badgeHtml}
${m.id}`; + row.innerHTML=`
${esc(modelName)}${badgeHtml}
${esc(m.id)}`; row.onclick=()=>selectModelFromDropdown(m.value); dd.appendChild(row); } @@ -1234,7 +1234,7 @@ function renderModelDropdown(){ const badgeHtml=m.badge?`${esc(m.badge.label||'Configured')}`:''; // Inline provider chip on every row that has a group (#1425) const providerChip=m.group?`${esc(m.group)}`:''; - row.innerHTML=`
${m.name}${badgeHtml}${providerChip}
${m.id}`; + row.innerHTML=`
${esc(m.name)}${badgeHtml}${providerChip}
${esc(m.id)}`; row.onclick=()=>selectModelFromDropdown(m.value); dd.appendChild(row); } diff --git a/tests/test_model_picker_escaping.py b/tests/test_model_picker_escaping.py new file mode 100644 index 00000000..76fcd44e --- /dev/null +++ b/tests/test_model_picker_escaping.py @@ -0,0 +1,20 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def test_model_picker_escapes_provider_supplied_model_labels(): + src = (ROOT / "static" / "ui.js").read_text() + + assert '${esc(m.id)}' in src + assert '${esc(m.name)}' in src + assert '${m.id}' not in src + assert '${m.name}' not in src + + +def test_providers_panel_escapes_load_error_text(): + src = (ROOT / "static" / "panels.js").read_text() + + assert "Failed to load providers: '+esc(e.message||String(e))+'" in src + assert "Failed to load providers: '+e.message+'" not in src