chat: fix bare backtick flicker in streamed agent host markdown#318498
Merged
roblourens merged 1 commit intoMay 27, 2026
Merged
Conversation
Streaming the agent host's worktree announcement
("Created isolated worktree for branch `xyz`") briefly rendered a bare
opening backtick because `fillInIncompleteTokens` was not closing the
codespan during progressive rendering. The progressive word renderer in
`chatWordCounter` deliberately excludes backticks from word characters,
so the intermediate state at the end of a streamed chunk is exactly
"…for branch \`xyz" — which is precisely what
`fillInIncompleteTokensOnce` is supposed to patch up.
Two issues conspired:
1. `ChatContentMarkdownRenderer` wraps `supportHtml` markdown in
`<body>...</body>` so dompurify does not strip leading comments.
That makes the lexer emit
`[html('<body>'), paragraph(…`xyz), space, html('</body>')]`, so
`tokens.at(-1)` is `html`, not `paragraph`, and the
codespan / list fix-ups never run.
2. The agent host paths were unconditionally setting
`supportHtml: true` on streamed markdown deltas. The
"supportHtml is load bearing" comment is now stale: PR #318053
replaced the old `AgentHostEditingSession` (which emitted bare
` ```` ` fences as `markdownContent`) with a dedicated
`'externalEdit'` progress part, so model deltas have nothing to
accidentally merge into.
Fixes:
- `fillInIncompleteTokensOnce` now walks past trailing `space` and
`html` tokens to find the last paragraph/list, then preserves those
trailing tokens around the patched-up node. Heading branch is
intentionally left alone.
- Drop `supportHtml: true` (and the stale comment) from both the live
streaming sink in `agentHostSessionHandler` and the history rebuild
in `stateToProgressAdapter`. Drop the now-unused `options` parameter
from `rawMarkdownToString`.
Tests:
- New token-level regression in `markdownRenderer.test.ts` exercises
the `<body>…</body>`-wrapped + bare-backtick scenario.
- New end-to-end test in `chatMarkdownRenderer.test.ts` runs through
the full render pipeline (with `supportHtml: true` and
`fillInIncompleteTokens: true`) and asserts a `<code>` element is
produced with no leftover bare backtick.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR addresses a rendering flicker in streamed agent-host markdown where partial content ending in a bare backtick briefly shows in the DOM before the closing backtick arrives. It does this by making fillInIncompleteTokens resilient to supportHtml-wrapped markdown (which introduces trailing html tokens like </body>), and by removing stale supportHtml: true usage in the agent host markdown emission paths.
Changes:
- Make
fillInIncompleteTokensOnceskip trailingspace/htmltokens when deciding which paragraph/list token to patch, while preserving those trailing tokens in the output token list. - Remove
supportHtml: truefrom agent-host markdown emission (streaming sink + history adapter) and simplifyrawMarkdownToStringaccordingly. - Add regression tests covering codespan completion with
supportHtmlwrapping at both token-level and end-to-end chat rendering levels.
Show a summary per file
| File | Description |
|---|---|
| src/vs/base/browser/markdownRenderer.ts | Adjusts incomplete-token fixup logic to operate correctly even when trailing html/space tokens exist (e.g. </body> wrapper). |
| src/vs/base/test/browser/markdownRenderer.test.ts | Adds a token-level regression test for codespan completion when markdown is wrapped in <body>...</body>. |
| src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentHostSessionHandler.ts | Removes supportHtml: true from streamed agent-host markdown delta emission. |
| src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/stateToProgressAdapter.ts | Removes supportHtml: true from markdown history conversion and drops the unused options parameter from rawMarkdownToString. |
| src/vs/workbench/contrib/chat/test/browser/widget/chatMarkdownRenderer.test.ts | Adds an end-to-end regression test ensuring bare codespans are closed when supportHtml wrapping introduces trailing html tokens. |
Copilot's findings
Comments suppressed due to low confidence (1)
src/vs/base/browser/markdownRenderer.ts:1033
fillInIncompleteTokensOncenow skips trailingspace/htmltokens for the list/paragraph fixups, but the heading fixup still usestokens.at(-1). ForsupportHtml-wrapped markdown (where</body>is the literal last token), the existing heading completion (used to avoidhello\n-being parsed as a setext heading) will never run. Consider locating the last heading token using the samelastInterestingIdxlogic (and preserving trailing tokens) so heading-related fixups also work when trailinghtml/spacetokens are present.
const lastToken = tokens.at(-1);
if (lastToken?.type === 'heading') {
const completeTokens = completeHeading(lastToken as marked.Tokens.Heading, mergeRawTokenText(tokens));
if (completeTokens) {
return completeTokens;
}
- Files reviewed: 5/5 changed files
- Comments generated: 0
anthonykim1
approved these changes
May 27, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes a bare-backtick flicker while streaming agent host responses, plus removes a now-stale
supportHtml: truefrom agent host markdown emission.Symptom
For agent host (AH) sessions, the initial worktree announcement
briefly rendered as
(bare opening backtick, no
<code>) until the closing backtick word arrived. The progressive word renderer (chatWordCounter) deliberately splits at non-backtick boundaries, so the intermediate string ending in a bare`is expected —fillInIncompleteTokensis supposed to handle that.Why
fillInIncompleteTokenswasn't firingChatContentMarkdownRendererwraps anysupportHtmlmarkdown in<body>...</body>(so dompurify keeps leading comments). The lexer then produces:fillInIncompleteTokensOnceonly inspectstokens.at(-1)for its paragraph / list / heading fix-ups. The trailinghtml('</body>')token meant the codespan fix-up never ran.Why AH was opting into
supportHtmlin the first placeThe
// supportHtml is load bearing…comment inagentHostSessionHandler._setupMarkdownPartwas stale. It dated back to whenAgentHostEditingSessionemitted file edits as amarkdownContent('\n````\n')+codeblockUri+textEdit+markdownContent('\n````\n')cluster — thesupportHtml: truetag on model deltas madecanMergeMarkdownStringsreject merging with those bare fence chunks. After #318053 (Connor) replaced that whole thing with a dedicated'externalEdit'progress part, model deltas have nothing to accidentally merge into.Fixes
Both layers are addressed:
Defensive (base layer).
fillInIncompleteTokensOncenow walks past trailingspace/htmltokens to find the last paragraph/list, then preserves those trailing tokens around the patched node. The heading branch is intentionally left alone. This means any future consumer ofsupportHtml: truestreaming markdown also gets correct fix-ups.Root (AH layer). Drop
supportHtml: truefrom:agentHostSessionHandler._setupMarkdownPart(live streaming sink) — and remove the stale "load bearing" comment.stateToProgressAdapter.turnsToHistory(ResponsePartKind.Markdowncase).Also drops the now-unused
optionsparameter fromrawMarkdownToString. This aligns the streaming and history paths with the already-differentactiveTurnToProgresspath (which never set the flag), and with vanilla Copilot chatstream.markdown(...)callsites.Tests
markdownRenderer.test.ts): assertsfillInIncompleteTokenson the<body>…\xyz\n\n` token list matches the lex of the balanced wrapped string.chatMarkdownRenderer.test.ts): renders aMarkdownStringwithsupportHtml: truecontainingCreated isolated worktree for branch \xyzthrough the fullChatContentMarkdownRenderer.render(md, { fillInIncompleteTokens: true })pipeline and asserts axyz` is in the output with no bare backtick in the text content.All 213 tests pass across the affected suites (markdownRenderer + chatMarkdownRenderer + stateToProgressAdapter);
tsgo --noEmitis clean.(Written by Copilot)