Skip to content

Commit 60a51fd

Browse files
committed
fix: strip leaked code blocks from chat when artifacts present
Two-layer fix for code content leaking into assistant messages: 1. Enhanced parser: _stripLeakedCodeBlocks() strips fenced code blocks from output when artifacts are detected in the stream 2. AssistantMessage safety net: regex strips code blocks at render time when artifact placeholders are present in the processed output
1 parent 6a44a4d commit 60a51fd

2 files changed

Lines changed: 58 additions & 9 deletions

File tree

app/components/chat/AssistantMessage.tsx

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,11 @@ interface AssistantMessageProps {
6868
}
6969

7070
const COMPLETE_ARTIFACT_BLOCK_RE = /<devonzArtifact[^>]*>[\s\S]*?<\/devonzArtifact>/g;
71+
const COMPLETE_ACTION_BLOCK_RE = /<devonzAction[^>]*>[\s\S]*?<\/devonzAction>/g;
7172
const UNCLOSED_ARTIFACT_RE = /<devonzArtifact[^>]*>[\s\S]*$/;
73+
const UNCLOSED_ACTION_RE = /<devonzAction[^>]*>[\s\S]*$/;
7274
const LEFTOVER_TAG_RE = /<\/?devonz(?:Artifact|Action)[^>]*>/g;
75+
const PARTIAL_DEVONZ_TAG_RE = /<devonz[A-Za-z]*(?:\s[^>]*)?$/;
7376

7477
/** Matches complete or partial `</assistant>` / `<assistant>` XML tags that LLMs sometimes emit */
7578
const ASSISTANT_TAG_RE = /<\/?assist(?:ant)?>|<\/assis(?:t(?:a(?:n(?:t)?)?)?)?\s*$/g;
@@ -78,22 +81,36 @@ const ASSISTANT_TAG_RE = /<\/?assist(?:ant)?>|<\/assis(?:t(?:a(?:n(?:t)?)?)?)?\s
7881
const CHAIN_OF_THOUGHT_BLOCK_RE = /<chain_of_thought>[\s\S]*?<\/chain_of_thought>/g;
7982
const CHAIN_OF_THOUGHT_TAG_RE = /<\/?chain_of_thought\s*\/?>/g;
8083

84+
/** Matches complete fenced code blocks that leaked outside artifact wrappers */
85+
const LEAKED_CODE_BLOCK_RE = /```[\w]*\n[\s\S]*?```/g;
86+
87+
/** Matches unclosed fenced code blocks still being streamed */
88+
const UNCLOSED_CODE_BLOCK_RE = /```[\w]*\n[\s\S]*$/;
89+
8190
/**
82-
* Strip raw artifact/action markup that leaks through the parser during
83-
* streaming. Complete blocks are removed first, then everything after an
84-
* unclosed `<devonzArtifact` tag (whose closing tag hasn't arrived yet) is
85-
* chopped — this prevents code content from appearing in the chat bubble
86-
* while the AI is still generating.
91+
* Strip raw artifact/action markup — including file CONTENT — that leaks
92+
* through the parser during streaming.
93+
*
94+
* 1. Complete `<devonzArtifact>` blocks (wrapper + all actions + content)
95+
* 2. Complete `<devonzAction>` blocks (handles cases where the parser
96+
* already consumed the artifact wrapper but left action tags + code)
97+
* 3. Everything after an unclosed `<devonzArtifact` or `<devonzAction`
98+
* tag (content still streaming)
99+
* 4. Leftover individual open/close tags
100+
* 5. Partial `<devonz...` tags mid-stream (e.g. `<devonzArt`)
87101
*
88-
* Also strips stray `</assistant>` or partial fragments like `</assis`
89-
* that some models emit at the end of their response.
102+
* Also strips stray `</assistant>` fragments and `<chain_of_thought>` blocks.
90103
*/
91104
function stripRawArtifactTags(text: string): string {
92105
let result = text;
93106

94-
if (result.includes('devonzA')) {
95-
result = result.replace(COMPLETE_ARTIFACT_BLOCK_RE, '').replace(UNCLOSED_ARTIFACT_RE, '');
107+
if (result.includes('<devonz')) {
108+
result = result.replace(COMPLETE_ARTIFACT_BLOCK_RE, '');
109+
result = result.replace(COMPLETE_ACTION_BLOCK_RE, '');
110+
result = result.replace(UNCLOSED_ARTIFACT_RE, '');
111+
result = result.replace(UNCLOSED_ACTION_RE, '');
96112
result = result.replace(LEFTOVER_TAG_RE, '');
113+
result = result.replace(PARTIAL_DEVONZ_TAG_RE, '');
97114
}
98115

99116
if (result.includes('assis') || result.includes('Assis')) {
@@ -104,6 +121,13 @@ function stripRawArtifactTags(text: string): string {
104121
result = result.replace(CHAIN_OF_THOUGHT_BLOCK_RE, '').replace(CHAIN_OF_THOUGHT_TAG_RE, '');
105122
}
106123

124+
// Strip leaked code blocks when artifacts are present — code content
125+
// should only appear inside artifact actions, never in chat text
126+
if (result.includes('__devonzArtifact__')) {
127+
result = result.replace(LEAKED_CODE_BLOCK_RE, '');
128+
result = result.replace(UNCLOSED_CODE_BLOCK_RE, '');
129+
}
130+
107131
return result;
108132
}
109133

app/lib/runtime/enhanced-message-parser.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,16 @@ export class EnhancedStreamingMessageParser extends StreamingMessageParser {
6666
output = super.parse(messageId, enhancedInput);
6767
logger.debug('After reset, output length:', output.length, 'resetFlag:', this._lastResetOccurred);
6868
}
69+
} else {
70+
// Artifacts ARE present — strip any markdown code blocks that leaked
71+
// outside artifact tags. These are file contents the AI included in
72+
// explanation text instead of inside <devonzArtifact> actions.
73+
const stripped = this._stripLeakedCodeBlocks(output);
74+
75+
if (stripped !== output) {
76+
logger.debug('Stripped leaked code blocks from output for message:', messageId);
77+
output = stripped;
78+
}
6979
}
7080

7181
return output;
@@ -540,6 +550,21 @@ ${content.trim()}
540550
});
541551
}
542552

553+
/**
554+
* Strip markdown fenced code blocks that leaked outside artifact tags.
555+
* When artifacts are present, any code blocks in the text output are
556+
* file contents that should have been inside artifact actions.
557+
*/
558+
private _stripLeakedCodeBlocks(output: string): string {
559+
// Strip complete fenced code blocks: ```lang\ncode\n```
560+
let result = output.replace(/```[\w]*\n[\s\S]*?```/g, '');
561+
562+
// Strip unclosed fenced code blocks at end (still streaming)
563+
result = result.replace(/```[\w]*\n[\s\S]*$/g, '');
564+
565+
return result;
566+
}
567+
543568
reset() {
544569
super.reset();
545570
this._processedCodeBlocks.clear();

0 commit comments

Comments
 (0)