The streaming path used to call _renderAssistantMarkdown on every incoming text delta, which re-ran marked.parse on the WHOLE accumulated markdown source and re-post-processed (added the Copy/Insert/Replace toolbar to) every <pre> block. This is a partial-render problem: a markdown source mid-stream (e.g. an opening ```py fence with no closing fence yet, or a partial heading) classifies DIFFERENTLY than the complete message. Each new delta could re-classify the same text into a different DOM shape, causing visible flicker - <pre>s appearing and disappearing, the toolbar popping in and out, paragraphs re-grouping - while the model is still typing. The bug surfaced because the user could see the glitchy view during a stream but everything looked correct after closing+reopening the panel (which goes through the static setMessages -> marked.parse path on the full text). Fix: keep the streaming path dumb. _appendToAssistantText now just appends the raw delta to the assistant element's textContent. The full markdown render happens exactly once at the end of the turn, inside _resetStreamPointers (which the prompt invokes when session.idle arrives, before calling onStreamEnd). Result: - The user still sees real-time text growth (textContent += delta). - No more intermediate-state flicker. - The rendered view at turn end is byte-for-byte equivalent to what setMessages would produce from the stored history (they share _renderAssistantMarkdown). The corresponding regression test is updated: it now asserts the DURING-stream state is raw text (no <pre>, no toolbar) and the POST-idle state is the rendered DOM with the toolbar attached. Tests: 72 jest (unchanged count, the one streaming test was rewritten), 52 pytest, build green. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -596,16 +596,19 @@ export class OpenCodeInlinePrompt extends Widget {
|
||||
if (!this._currentAssistantEl) {
|
||||
this._currentAssistantEl = this._createMessageElement('assistant', '');
|
||||
}
|
||||
// Accumulate the raw markdown source, then re-render the whole
|
||||
// message through marked.parse. This way the user sees properly
|
||||
// formatted output (code blocks as <pre><code>, headings, lists)
|
||||
// in real-time as text deltas arrive — NOT the raw ```fence```
|
||||
// source that an append-only .textContent would show.
|
||||
// During streaming, append the raw delta to the assistant element's
|
||||
// textContent. We do NOT run marked.parse on every delta: the
|
||||
// partial source (e.g. an opening ```py fence with no closing
|
||||
// fence yet) is structurally different from the complete message
|
||||
// and the toolbar post-processing creates visible flicker —
|
||||
// <pre>s appear and disappear as the renderer re-classifies the
|
||||
// incomplete markdown between deltas. The user gets stable
|
||||
// real-time text here, and the full markdown render happens once
|
||||
// at the end of the turn (see _resetStreamPointers) so the result
|
||||
// is identical to what setMessages would produce from the stored
|
||||
// history.
|
||||
this._currentAssistantText += text;
|
||||
this._renderAssistantMarkdown(
|
||||
this._currentAssistantEl,
|
||||
this._currentAssistantText
|
||||
);
|
||||
this._currentAssistantEl.textContent = this._currentAssistantText;
|
||||
this._scrollToBottom();
|
||||
}
|
||||
|
||||
@@ -807,6 +810,19 @@ export class OpenCodeInlinePrompt extends Widget {
|
||||
}
|
||||
|
||||
private _resetStreamPointers(): void {
|
||||
// Finalize the in-progress assistant message by running the
|
||||
// markdown render ONCE on the accumulated raw text. This is the
|
||||
// moment where the streaming path converges to the same DOM
|
||||
// structure that setMessages would produce from the stored
|
||||
// history (i.e. identical to "reopen" behavior). Without this
|
||||
// final pass the user would see raw ```fence``` source
|
||||
// permanently until they close+reopen the panel.
|
||||
if (this._currentAssistantEl && this._currentAssistantText) {
|
||||
this._renderAssistantMarkdown(
|
||||
this._currentAssistantEl,
|
||||
this._currentAssistantText
|
||||
);
|
||||
}
|
||||
this._currentAssistantEl = null;
|
||||
this._currentAssistantText = '';
|
||||
this._activeBlocks = {};
|
||||
|
||||
Reference in New Issue
Block a user