fix: decode HTML entities before markdown processing - #239
Conversation
Prevents double-escaping when LLM outputs HTML entities like <code> in markdown bold/code syntax. Without decode, esc() would turn < into &lt; resulting in literal &lt;code&gt; being displayed instead of <code>. Fixes rendering of strong and code tags when content contains HTML entities.
…ant) - Fill in all 40+ missing Simplified Chinese (zh) translations - Add new zh-Hant (Traditional Chinese) locale with full translation - Missing keys covered: tab labels, settings descriptions, boot/messages/ui strings Breaks down as: - zh: 123 → 164 keys (+41) - zh-Hant: new locale with 163 keys
Differentiates from 繁體中文 (Traditional Chinese) in language selector.
|
Thanks for this PR @Argonaut790 — two meaningful contributions bundled together. HTML entity decode fixThe double-escaping bug is real: when an LLM outputs something like A couple of things to verify before merging: 1. Decode scope — 2. XSS surface — HTML entity decoding on untrusted LLM output can expand Chinese translations (zh + zh-Hant)The translation additions look solid — 41 missing keys filled in for Simplified Chinese and a complete Traditional Chinese locale added. The rename from One thing to check: are the Summary
Could you confirm the call order in |
Call Order Verification: decode → esc ✓Confirmed the call order in
XSS SafetyThe decode-first order is safe because:
Only allowlisted tags ( zh-Hant Translations: Machine-Assisted, Needs Native ReviewThe Traditional Chinese translations were generated by AI (based on zh locale + Taiwan/HK terminology conventions), then manually reviewed and refined. I can flag this in the PR for native speakers to correct any unnatural phrasing. Common terms I used:
Native TW/HK speakers: please review and correct any terminology that feels unnatural! 🙏 |
|
Thanks for the detailed follow-up, @Argonaut790 — that answers both questions clearly. Call order confirmed ✅The decode → allowlist-filter → esc order is exactly right. Decoding first, then passing only the six allowlisted tags through before zh-Hant translation source noted ✅Good call flagging it as AI-assisted with manual review. The terminology choices you listed ( This looks good to merge. The two blocking items from the initial review are resolved:
|
Full Review: PR #239 — decode HTML entities + Chinese translationsThanks @Argonaut790! Two contributions bundled together — entity decode fix and Chinese locale work. Security Audit — HTML Entity DecodeThis needs careful scrutiny. The const decode=s=>s.replace(/</g,'<').replace(/>/g,'>').replace(/&/g,'&')
.replace(/"/g,'"').replace(/'/g,"'");
s=decode(s);This converts The contributor and agent confirmed the call order is safe: decode → allowlist filter → However, there's a subtle risk: if ANY code path in Code Review — Entity DecodeThe fix is correct for the described problem: when LLMs output The decode function only handles the 5 standard HTML entities — no custom entities, no numeric entities. This is intentionally conservative. Code Review — Chinese Translations
One issue: The Also, there are duplicate keys at the bottom of both Tests554 passed, 0 failed, 42 skipped. No regressions. VerdictThe entity decode fix is sound. The Chinese translations have character-set inconsistencies (Traditional chars in Simplified locale) and duplicate keys that should be cleaned up. Approve the entity decode; the i18n portion needs a polish pass. |
…nslations (#239) Adds decode() helper in renderMd() to fix double-escaping of HTML entities from LLM output (e.g. <code> becoming &lt;code&gt; instead of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns. Also adds 40+ missing zh (Simplified Chinese) translation keys and a new zh-Hant (Traditional Chinese) locale with 163 keys. Fix applied: removed duplicate settings_label_notifications key in both zh and zh-Hant locales. Fixes #240
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (#239) Adds decode() helper in renderMd() to fix double-escaping of HTML entities from LLM output (e.g. <code> becoming &lt;code&gt; instead of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns. Also adds 40+ missing zh (Simplified Chinese) translation keys and a new zh-Hant (Traditional Chinese) locale with 163 keys. Fix applied: removed duplicate settings_label_notifications key in both zh and zh-Hant locales. Fixes #240 * fix: restore custom model list discovery with config api key (#238) get_available_models() now reads api_key from config.yaml before env vars: 1. model.api_key 2. providers.<active>.api_key / providers.custom.api_key 3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.) Also adds OpenAI/Python User-Agent header and a regression test covering authenticated /v1/models discovery. Fixes users with LM Studio / Ollama custom endpoints configured in config.yaml whose model picker silently collapsed to the default model. * feat: Docker UID/GID matching to avoid root-owned .hermes files (#237) Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so container files match the host user UID/GID. Prevents .hermes volume mounts from being owned by root when using a non-root host user. Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000). Readme updated with setup instructions. Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml that was overriding the ${GID:-1000} variable expansion. * security: redact credentials from API responses and fix credential file permissions (#243) Adds response-layer credential redaction to three endpoints: - GET /api/session — messages[], tool_calls[], and title - GET /api/session/export — download also redacted - SSE done event — session payload in stream - GET /api/memory — MEMORY.md and USER.md content Adds api/startup.py with fix_credential_permissions() at server startup. Adds 13 tests in tests/test_security_redaction.py. Merged with #237 container detection changes in server.py. * fix: cancel button now interrupts agent and cleans up UI state (#244) Wires agent.interrupt() into cancel_stream() so the backend actually stops tool execution when the user clicks Cancel, rather than only stopping the SSE stream while the agent keeps running. Changes: - api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent) - api/streaming.py: stores agent in AGENT_INSTANCES after creation, checks CANCEL_FLAGS immediately after store (race condition fix), calls agent.interrupt() in cancel_stream(), cleans up in finally block - static/boot.js: removes stale setStatus(cancelling) call - static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel Race condition fix: after storing agent in AGENT_INSTANCES, immediately checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during agent init) and interrupts before starting. Check is inside the same STREAMS_LOCK acquisition, making it atomic. New test file: tests/test_cancel_interrupt.py with 6 unit tests. * docs: v0.46.0 release notes, bump version, update test counts --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
|
Agent review — APPROVED ✅ (merged to v0.46.0) Reviewed full diff, PR comments, and discussion thread. What this fixes: Double-escaping of HTML entities from LLM output in Fix applied during review: Removed a duplicate zh-Hant locale: Properly structured with Tests: 624 passed on stage (up from 604 — this PR contributes no new test files but all existing tests pass). Merged to master via stage branch in PR #249 (v0.46.0). |
- @Argonaut790 (#239): HTML entity decode fix + Traditional Chinese locale (fix shipped in v0.46.0; zh-Hant locale added same PR) - @indigokarasu (#213): CSS-only visual redesign proposal — design token system + icon rail + 7 themes (influenced v0.50.0 design language) - @zenc-cp (#133): Anti-hallucination guard for ReAct loop — streaming token buffer + post-run scrub pattern README now has 33 contributors covering full project history.
… (complete to 33) - @Argonaut790 (#239): HTML entity decode fix + Traditional Chinese locale (fix shipped in v0.46.0; zh-Hant locale added same PR) - @indigokarasu (#213): CSS-only visual redesign proposal — design token system + icon rail + 7 themes (influenced v0.50.0 design language) - @zenc-cp (#133): Anti-hallucination guard for ReAct loop — streaming token buffer + post-run scrub pattern README now has 33 contributors covering full project history. Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
|
Your PR (#239) shipped two things: the HTML entity decode fix in |
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (nesquena#239) Adds decode() helper in renderMd() to fix double-escaping of HTML entities from LLM output (e.g. <code> becoming &lt;code&gt; instead of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns. Also adds 40+ missing zh (Simplified Chinese) translation keys and a new zh-Hant (Traditional Chinese) locale with 163 keys. Fix applied: removed duplicate settings_label_notifications key in both zh and zh-Hant locales. Fixes nesquena#240 * fix: restore custom model list discovery with config api key (nesquena#238) get_available_models() now reads api_key from config.yaml before env vars: 1. model.api_key 2. providers.<active>.api_key / providers.custom.api_key 3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.) Also adds OpenAI/Python User-Agent header and a regression test covering authenticated /v1/models discovery. Fixes users with LM Studio / Ollama custom endpoints configured in config.yaml whose model picker silently collapsed to the default model. * feat: Docker UID/GID matching to avoid root-owned .hermes files (nesquena#237) Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so container files match the host user UID/GID. Prevents .hermes volume mounts from being owned by root when using a non-root host user. Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000). Readme updated with setup instructions. Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml that was overriding the ${GID:-1000} variable expansion. * security: redact credentials from API responses and fix credential file permissions (nesquena#243) Adds response-layer credential redaction to three endpoints: - GET /api/session — messages[], tool_calls[], and title - GET /api/session/export — download also redacted - SSE done event — session payload in stream - GET /api/memory — MEMORY.md and USER.md content Adds api/startup.py with fix_credential_permissions() at server startup. Adds 13 tests in tests/test_security_redaction.py. Merged with nesquena#237 container detection changes in server.py. * fix: cancel button now interrupts agent and cleans up UI state (nesquena#244) Wires agent.interrupt() into cancel_stream() so the backend actually stops tool execution when the user clicks Cancel, rather than only stopping the SSE stream while the agent keeps running. Changes: - api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent) - api/streaming.py: stores agent in AGENT_INSTANCES after creation, checks CANCEL_FLAGS immediately after store (race condition fix), calls agent.interrupt() in cancel_stream(), cleans up in finally block - static/boot.js: removes stale setStatus(cancelling) call - static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel Race condition fix: after storing agent in AGENT_INSTANCES, immediately checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during agent init) and interrupts before starting. Check is inside the same STREAMS_LOCK acquisition, making it atomic. New test file: tests/test_cancel_interrupt.py with 6 unit tests. * docs: v0.46.0 release notes, bump version, update test counts --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
… (complete to 33) - @Argonaut790 (nesquena#239): HTML entity decode fix + Traditional Chinese locale (fix shipped in v0.46.0; zh-Hant locale added same PR) - @indigokarasu (nesquena#213): CSS-only visual redesign proposal — design token system + icon rail + 7 themes (influenced v0.50.0 design language) - @zenc-cp (nesquena#133): Anti-hallucination guard for ReAct loop — streaming token buffer + post-run scrub pattern README now has 33 contributors covering full project history. Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
* fix: decode HTML entities before markdown processing + zh/zh-Hant translations (nesquena#239) Adds decode() helper in renderMd() to fix double-escaping of HTML entities from LLM output (e.g. <code> becoming &lt;code&gt; instead of rendering). XSS-safe: decode runs before esc(), only 5 entity patterns. Also adds 40+ missing zh (Simplified Chinese) translation keys and a new zh-Hant (Traditional Chinese) locale with 163 keys. Fix applied: removed duplicate settings_label_notifications key in both zh and zh-Hant locales. Fixes nesquena#240 * fix: restore custom model list discovery with config api key (nesquena#238) get_available_models() now reads api_key from config.yaml before env vars: 1. model.api_key 2. providers.<active>.api_key / providers.custom.api_key 3. env var fallbacks (HERMES_API_KEY, OPENAI_API_KEY, etc.) Also adds OpenAI/Python User-Agent header and a regression test covering authenticated /v1/models discovery. Fixes users with LM Studio / Ollama custom endpoints configured in config.yaml whose model picker silently collapsed to the default model. * feat: Docker UID/GID matching to avoid root-owned .hermes files (nesquena#237) Adds docker_init.bash with hermeswebuitoo/hermeswebui user pattern so container files match the host user UID/GID. Prevents .hermes volume mounts from being owned by root when using a non-root host user. Configure via WANTED_UID and WANTED_GID env vars (default 1000/1000). Readme updated with setup instructions. Fix applied: removed duplicate WANTED_GID=1000 line in docker-compose.yml that was overriding the ${GID:-1000} variable expansion. * security: redact credentials from API responses and fix credential file permissions (nesquena#243) Adds response-layer credential redaction to three endpoints: - GET /api/session — messages[], tool_calls[], and title - GET /api/session/export — download also redacted - SSE done event — session payload in stream - GET /api/memory — MEMORY.md and USER.md content Adds api/startup.py with fix_credential_permissions() at server startup. Adds 13 tests in tests/test_security_redaction.py. Merged with nesquena#237 container detection changes in server.py. * fix: cancel button now interrupts agent and cleans up UI state (nesquena#244) Wires agent.interrupt() into cancel_stream() so the backend actually stops tool execution when the user clicks Cancel, rather than only stopping the SSE stream while the agent keeps running. Changes: - api/config.py: adds AGENT_INSTANCES dict (stream_id -> AIAgent) - api/streaming.py: stores agent in AGENT_INSTANCES after creation, checks CANCEL_FLAGS immediately after store (race condition fix), calls agent.interrupt() in cancel_stream(), cleans up in finally block - static/boot.js: removes stale setStatus(cancelling) call - static/messages.js: setBusy(false)/setStatus('') unconditionally on cancel Race condition fix: after storing agent in AGENT_INSTANCES, immediately checks if CANCEL_FLAGS[stream_id] is already set (cancel arrived during agent init) and interrupts before starting. Check is inside the same STREAMS_LOCK acquisition, making it atomic. New test file: tests/test_cancel_interrupt.py with 6 unit tests. * docs: v0.46.0 release notes, bump version, update test counts --------- Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
… (complete to 33) - @Argonaut790 (nesquena#239): HTML entity decode fix + Traditional Chinese locale (fix shipped in v0.46.0; zh-Hant locale added same PR) - @indigokarasu (nesquena#213): CSS-only visual redesign proposal — design token system + icon rail + 7 themes (influenced v0.50.0 design language) - @zenc-cp (nesquena#133): Anti-hallucination guard for ReAct loop — streaming token buffer + post-run scrub pattern README now has 33 contributors covering full project history. Co-authored-by: Nathan Esquenazi <nesquena@gmail.com>
Fix: HTML Entity Double-Escaping in Markdown Rendering
Problem
When LLM outputs HTML entities like
<code>in markdown bold/code syntax, theesc()function was escaping them again, turning<into&lt;resulting in literal text being displayed instead of rendered HTML.Solution
Added HTML entity decode step before markdown processing in
renderMd()function (static/ui.js).Feat: Complete Chinese Translations + Traditional Chinese Locale
Changes
Missing keys now covered:
Files Changed
static/ui.js: Added decode() helper for HTML entitiesstatic/i18n.js: Completed zh translations + added zh-Hant locale