Skip to content

fix(web): preserve XML-like tags in user messages - #4133

Merged
juliusmarminge merged 2 commits into
pingdotgg:mainfrom
0utsights:codex/issue-4059-rendering-repro
Aug 15, 2026
Merged

fix(web): preserve XML-like tags in user messages#4133
juliusmarminge merged 2 commits into
pingdotgg:mainfrom
0utsights:codex/issue-4059-rendering-repro

Conversation

@0utsights

@0utsights 0utsights commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • display user-authored HTML/XML-like syntax literally in chat messages
  • preserve sanitized raw HTML rendering for assistant messages
  • add regression coverage for both behaviors

Root cause

ChatMarkdown always enabled raw HTML parsing and sanitization. Unknown XML-like elements in user input, such as <global-agent-instructions>, were therefore interpreted as HTML and removed from the rendered message.

User messages now opt out of raw HTML parsing, so their original text remains visible. Assistant messages keep the existing sanitized HTML behavior.

User impact

The rendered user message now matches what was typed instead of silently omitting XML-like tags. This only changes presentation; the message payload sent to the agent was already correct.

Fixes #4059.

Validation

  • pnpm exec vp test apps/web/src/components/chat/MessagesTimeline.test.tsx (14 tests passed)
  • pnpm exec vp check (0 errors; 10 existing warnings)
  • pnpm exec vp run typecheck

Note

Preserve XML-like tags in user messages by disabling raw HTML parsing

  • Sets parseRawHtml={false} on all ChatMarkdown usages in MessagesTimeline.tsx that render user-provided text, so XML-like tags and HTML are displayed as escaped source text rather than parsed.
  • Adds a parseRawHtml prop to ChatMarkdown (defaults to true) that conditionally applies rehype plugins; when false, raw HTML is not processed.
  • Drops title attributes from rendered <a> and <img> elements in markdown output.
  • Behavioral Change: user messages no longer render any HTML, even sanitized HTML — only assistant messages retain HTML parsing behavior.

Macroscope summarized 9e2aa9b.


Note

Low Risk
Presentation-only change for user messages with a clear security win; assistant markdown behavior unchanged.

Overview
User chat messages now render HTML/XML-like syntax as escaped literal text instead of parsing it as HTML, so tags such as <global-agent-instructions> and comparisons like 2 < 3 stay visible instead of disappearing.

ChatMarkdown gains a parseRawHtml flag (default true). When false, rehype raw HTML parsing/sanitization is skipped and skipHtml={false} keeps unparsed HTML as text. All user-message ChatMarkdown usages in MessagesTimeline pass parseRawHtml={false}; assistant messages keep sanitized raw HTML (e.g. <details>).

Link/image renderers drop markdown title attributes from the DOM. Regression tests cover user literal tags, XSS as text, and assistant sanitized HTML.

Reviewed by Cursor Bugbot for commit 9e2aa9b. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5f1f2543-a5f7-4609-bcff-7812fc6d708d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added size:XS 0-9 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 18, 2026
@0utsights
0utsights marked this pull request as ready for review July 19, 2026 00:50

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6fc3bad. Configure here.

Comment thread apps/web/src/components/ChatMarkdown.tsx
@macroscopeapp

macroscopeapp Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR changes how HTML is rendered in user messages (escaping vs parsing), which affects runtime behavior in a security-sensitive area. The unresolved review comment identifies a real gap where block-level HTML tags cause line breaks to collapse and miss paragraph styling.

You can customize Macroscope's approvability policy. Learn more.

@juliusmarminge
juliusmarminge force-pushed the codex/issue-4059-rendering-repro branch from 6fc3bad to f0da0f7 Compare July 20, 2026 17:33
juliusmarminge pushed a commit to 0utsights/t3code that referenced this pull request Jul 20, 2026
Render user-authored XML-like source as escaped text while retaining sanitized assistant HTML, with regression coverage for custom tags, code, comparisons, and unsafe input.

Co-authored-by: codex <codex@users.noreply.github.com>
@github-actions github-actions Bot added size:S 10-29 changed lines (additions + deletions). and removed size:XS 0-9 changed lines (additions + deletions). labels Jul 20, 2026
macroscopeapp[bot]
macroscopeapp Bot previously approved these changes Jul 20, 2026
Render user-authored XML-like source as escaped text while retaining sanitized assistant HTML, with regression coverage for custom tags, code, comparisons, and unsafe input.

Co-authored-by: codex <codex@users.noreply.github.com>
@juliusmarminge
juliusmarminge force-pushed the codex/issue-4059-rendering-repro branch from 79e6b9c to 6a9a0e8 Compare August 15, 2026 11:38
@macroscopeapp
macroscopeapp Bot dismissed their stale review August 15, 2026 11:38

Dismissing prior approval to re-evaluate 6a9a0e8

@juliusmarminge
juliusmarminge enabled auto-merge (squash) August 15, 2026 11:40

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One consistency issue found in the parseRawHtml switch: disabling the whole rehype chain also disables the sanitize schema, which is the only thing enforcing the project's no-native-title policy on rendered markdown. See the inline comment.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/ChatMarkdown.tsx
Co-authored-by: codex <codex@users.noreply.github.com>

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

One finding on the literal-HTML rendering path: block-level HTML in user messages now renders as an unwrapped, whitespace-collapsed text run. The title issue flagged on the previous commit is resolved by the new a/img renderers.

Posted via Macroscope — UI Consistency

lineBreaks ? CHAT_MARKDOWN_REMARK_PLUGINS_WITH_BREAKS : CHAT_MARKDOWN_REMARK_PLUGINS
}
rehypePlugins={CHAT_MARKDOWN_REHYPE_PLUGINS}
rehypePlugins={parseRawHtml ? CHAT_MARKDOWN_REHYPE_PLUGINS : undefined}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

With parseRawHtml={false} react-markdown replaces each raw node with one text node at the original node's position. That is fine for inline HTML (it stays inside the paragraph), but a block-level HTML node — a user message whose line starts with a tag, e.g.

<global-agent-instructions>
Do the thing
</global-agent-instructions>

is a single mdast html node covering the whole block, so the escaped source is emitted as a bare text node directly under .chat-markdown, outside any <p>. Two visible consequences: the typed newlines collapse to spaces (the default user-message path renders <ChatMarkdown> without a whitespace-pre-wrap ancestor, unlike the terminal-context path at MessagesTimeline.tsx:1776/1815, so the same message renders with different line structure depending on attached contexts), and the block gets none of the .chat-markdown p block spacing (index.css:1589). remarkBreaks cannot compensate because it only rewrites text nodes, not html nodes.

Smallest fix that keeps the literal-escaping goal: instead of relying on react-markdown's raw→text fallback, run a small remark plugin in literal mode (ordered before remarkBreaks) that rewrites mdast html nodes into text nodes, wrapping root-level ones in a paragraph. The escaping stays identical, and line breaks plus paragraph rhythm match the rest of the user message. Worth covering with a test for a multi-line HTML block, since the current tests only exercise single-line/inline cases.

Posted via Macroscope — UI Consistency

@juliusmarminge juliusmarminge left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Re-approved after the focused regression fix and verification.

@juliusmarminge
juliusmarminge merged commit cf7bfd1 into pingdotgg:main Aug 15, 2026
15 of 17 checks passed
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Aug 15, 2026
## What's Changed
* fix(desktop): app zoom no longer zooms the preview browser by @juliusmarminge in pingdotgg/t3code#6649
* fix(server): keep provider notification consumers alive past startSession by @tsouth89 in pingdotgg/t3code#6538
* fix(server): treat removed Bitbucket permissions endpoint as unknown, not blocking by @lnieuwenhuis in pingdotgg/t3code#6525
* fix(ssh): let cold remote servers finish starting by @gbarros-dev in pingdotgg/t3code#6168
* fix(web): preserve Claude insight line breaks by @nateEc in pingdotgg/t3code#4344
* feat(web): accept file drops across the chat workspace by @dbalders in pingdotgg/t3code#6636
* fix(web): widen ordered-list marker gutter for 3+ digit item numbers by @lnieuwenhuis in pingdotgg/t3code#6527
* fix(server): bound thread activity hydration by @t3-code[bot] in pingdotgg/t3code#6153
* fix(web): restore the Archive action in the default sidebar thread menu by @lnieuwenhuis in pingdotgg/t3code#6526
* fix(web): open diff files from nested projects by @gbarros-dev in pingdotgg/t3code#6174
* fix(mobile): use tryOpenExternalUrl for markdown links in ThreadFeed by @mohamedmastouri-hue in pingdotgg/t3code#5872
* fix(web): open the file a bare filename reference names by @Brechard in pingdotgg/t3code#6297
* fix(server): stop the provider title mirror from overwriting real thread titles by @repparw in pingdotgg/t3code#5941
* fix(shared): match source-control providers by DNS label by @gbarros-dev in pingdotgg/t3code#6175
* feat(desktop): Chrome-style hold-to-quit by @Bil0000 in pingdotgg/t3code#5508
* fix(gitlab): submit review comments on context lines by @tarik02 in pingdotgg/t3code#6348
* fix(marketing): keep Grok mark clear of mobile hero copy by @NicL9923 in pingdotgg/t3code#4542
* fix(mobile): recover the QR pairing scanner when camera access is denied by @hey-jj in pingdotgg/t3code#6487
* fix(web): keep a long path from running under the folder picker button by @Sy-D in pingdotgg/t3code#4823
* fix(terminal): right-click paste works in the terminal by @StiensWout in pingdotgg/t3code#5240
* fix(mobile): explain iOS-only settings on Android by @danvernon in pingdotgg/t3code#4981
* fix(web): stop counting a workflow coordinator as a working agent by @Rishet11 in pingdotgg/t3code#6672
* fix(web): keep floating preview anchored after panel closes by @AksharP5 in pingdotgg/t3code#6547
* fix(web): unstick /connect after in-modal sign-in by redirecting to the authorize endpoint by @TorbenWetter in pingdotgg/t3code#5133
* fix(web): keep send reachable while a turn is running on mobile by @AMohamedAakhil in pingdotgg/t3code#4781
* fix(web): reject unsupported composer image types at attach time by @mdshzb04 in pingdotgg/t3code#6574
* Make ClaudeTextGeneration tests hermetic on Windows by @mihneaptu in pingdotgg/t3code#4508
* fix(web): show command output in work log by @LikoKiko in pingdotgg/t3code#4083
* fix(web): reserve sibling column width when resizing the right panel by @Lasdw6 in pingdotgg/t3code#6279
* fix(web): replace whitespace in new ref names with dashes by @jorj-pineda in pingdotgg/t3code#6270
* fix(client-runtime): branch list no longer resets while paging through refs by @a20hek in pingdotgg/t3code#5858
* fix(web): support Shift+Insert terminal paste by @aoright in pingdotgg/t3code#5982
* fix(web): keep the composer glass aligned with the context strip at any interface font size by @Williawar in pingdotgg/t3code#5703
* fix(codex): keep background memory out of chats by @AksharP5 in pingdotgg/t3code#5468
* fix(server): treat a missing Codex rollout as a recoverable resume error by @Rishet11 in pingdotgg/t3code#6671
* fix(web): hide provider Update toast action while an update is running by @mrmg in pingdotgg/t3code#6544
* fix(desktop): agent shells inherit a UTF-8 locale on macOS by @Linus-Boehm in pingdotgg/t3code#6236
* fix(server): ignore Claude command lifecycle messages by @naveed949 in pingdotgg/t3code#6606
* docs: mention Bitbucket user read scope needed by auth probe by @thamrx in pingdotgg/t3code#6291
* fix(server): return valid preview action results by @duncan-vc in pingdotgg/t3code#5966
* fix(claude): make "Always allow for session" stick, and only for the session by @kakismash in pingdotgg/t3code#5041
* fix(ssh): surface a failed remote t3 install instead of a silent 0-byte server.log by @TorbenWetter in pingdotgg/t3code#5132
* perf(server): persist the wire projection for streaming tool.updated data by @mInrOz in pingdotgg/t3code#6675
* fix(web): stop wrapping partial code block selections in markdown fences by @JoeJoeflyn in pingdotgg/t3code#5069
* fix(web): hide T3 Connect toggle in web app settings by @JoeJoeflyn in pingdotgg/t3code#5068
* fix(web): show provider account accent badge in sidebar rows and hover card by @vitalyiegorov in pingdotgg/t3code#5980
* fix(server): wait for concurrent SQLite writers instead of failing with SQLITE_BUSY by @ostapondo in pingdotgg/t3code#5134
* fix(web): reject oversized prompts before provider turn start by @naveed949 in pingdotgg/t3code#6602
* feat(web): collapse the question prompt from its header by @Jardo-51 in pingdotgg/t3code#6773
* fix(shared): degrade an unknown system time zone to UTC in usage windows by @Rishet11 in pingdotgg/t3code#6670
* fix(claude): discover repo-local .agents/skills in skill discovery by @RoshanMhatre in pingdotgg/t3code#5488
* fix(server): let slow provider CLIs raise their discovery probe budget by @CDVolvik in pingdotgg/t3code#6223
* fix(web): retain terminal PR badges after checkout switch by @sebbonit in pingdotgg/t3code#4755
* fix(web): show selected model in context window tooltip by @nqrwhal in pingdotgg/t3code#4772
* fix(web): scale command details with code font by @Serendeep in pingdotgg/t3code#6510
* fix(web): preserve XML-like tags in user messages by @0utsights in pingdotgg/t3code#4133

## New Contributors
* @mohamedmastouri-hue made their first contribution in pingdotgg/t3code#5872
* @NicL9923 made their first contribution in pingdotgg/t3code#4542
* @hey-jj made their first contribution in pingdotgg/t3code#6487
* @danvernon made their first contribution in pingdotgg/t3code#4981
* @Rishet11 made their first contribution in pingdotgg/t3code#6672
* @AksharP5 made their first contribution in pingdotgg/t3code#6547
* @TorbenWetter made their first contribution in pingdotgg/t3code#5133
* @AMohamedAakhil made their first contribution in pingdotgg/t3code#4781
* @mdshzb04 made their first contribution in pingdotgg/t3code#6574
* @mihneaptu made their first contribution in pingdotgg/t3code#4508
* @LikoKiko made their first contribution in pingdotgg/t3code#4083
* @Lasdw6 made their first contribution in pingdotgg/t3code#6279
* @jorj-pineda made their first contribution in pingdotgg/t3code#6270
* @a20hek made their first contribution in pingdotgg/t3code#5858
* @aoright made their first contribution in pingdotgg/t3code#5982
* @Williawar made their first contribution in pingdotgg/t3code#5703
* @mrmg made their first contribution in pingdotgg/t3code#6544
* @Linus-Boehm made their first contribution in pingdotgg/t3code#6236
* @naveed949 made their first contribution in pingdotgg/t3code#6606
* @thamrx made their first contribution in pingdotgg/t3code#6291
* @duncan-vc made their first contribution in pingdotgg/t3code#5966
* @kakismash made their first contribution in pingdotgg/t3code#5041
* @mInrOz made their first contribution in pingdotgg/t3code#6675
* @JoeJoeflyn made their first contribution in pingdotgg/t3code#5069
* @vitalyiegorov made their first contribution in pingdotgg/t3code#5980
* @ostapondo made their first contribution in pingdotgg/t3code#5134
* @Jardo-51 made their first contribution in pingdotgg/t3code#6773
* @RoshanMhatre made their first contribution in pingdotgg/t3code#5488
* @CDVolvik made their first contribution in pingdotgg/t3code#6223
* @sebbonit made their first contribution in pingdotgg/t3code#4755
* @nqrwhal made their first contribution in pingdotgg/t3code#4772
* @Serendeep made their first contribution in pingdotgg/t3code#6510
* @0utsights made their first contribution in pingdotgg/t3code#4133

**Full Changelog**: pingdotgg/t3code@v0.0.34-nightly.20260815.1100...v0.0.34-nightly.20260815.1101

Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.0.34-nightly.20260815.1101
juliusmarminge added a commit that referenced this pull request Aug 15, 2026
Re-applies the deltas that mid-stack blob reverts discarded, and merges
main's work into the v2-owned surfaces:

- keybindings: main's STATIC_KEYBINDING_COMMANDS rename plus both new
  commands (rightPanel.toggleMaximized alongside threadPanel.toggle)
- OpenInPicker: main's remote-open/SSH routing and favorite-editor
  shortcut layered onto the branch's panel/toolbar variants; the
  extracted shouldShowOpenInPicker now takes remoteOpenMode
- ChatMarkdown: main's bare-filename resolver (#6297) ported into the
  branch's module-level component factory, plus #4133 title-attribute
  stripping on links and images
- ComposerPrimaryActions: main's #4781 model (stop stays reachable, send
  joins it when Enter-to-send is unavailable) carrying the branch's
  steering send button
- ComposerPendingUserInputPanel: main's collapsible redesign with the v2
  RuntimeRequestId and responseCapability gate
- ChatComposer: main's oversized-prompt submission guard wrapping the
  branch's dispatch-mode send
- preview shell: main's container-aware width clamp ported into the
  branch's usePreviewPanelInlineSize hook
- MessagesTimeline/Sidebar: main's day-aware timestamps, code-font tool
  bodies and provider accent badges on the v2 runtime shell
- index.css: main's @variant dark migration (#6381) replaces the branch's
  standalone .dark block
- contracts: main's send-turn image mime allowlist re-homed to
  chatAttachment.ts, where v2 keeps the other send-turn limits

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:S 10-29 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: XML-like tags get filtered/removed from the chat

2 participants