@@ -71,21 +71,30 @@ const COMPLETE_ARTIFACT_BLOCK_RE = /<devonzArtifact[^>]*>[\s\S]*?<\/devonzArtifa
7171const UNCLOSED_ARTIFACT_RE = / < d e v o n z A r t i f a c t [ ^ > ] * > [ \s \S ] * $ / ;
7272const LEFTOVER_TAG_RE = / < \/ ? d e v o n z (?: A r t i f a c t | A c t i o n ) [ ^ > ] * > / g;
7373
74+ /** Matches complete or partial `</assistant>` / `<assistant>` XML tags that LLMs sometimes emit */
75+ const ASSISTANT_TAG_RE = / < \/ ? a s s i s t (?: a n t ) ? > | < \/ a s s i s (?: t (?: a (?: n (?: t ) ? ) ? ) ? ) ? \s * $ / g;
76+
7477/**
7578 * Strip raw artifact/action markup that leaks through the parser during
7679 * streaming. Complete blocks are removed first, then everything after an
7780 * unclosed `<devonzArtifact` tag (whose closing tag hasn't arrived yet) is
7881 * chopped — this prevents code content from appearing in the chat bubble
7982 * while the AI is still generating.
83+ *
84+ * Also strips stray `</assistant>` or partial fragments like `</assis`
85+ * that some models emit at the end of their response.
8086 */
8187function stripRawArtifactTags ( text : string ) : string {
82- if ( ! text . includes ( 'devonzA' ) ) {
83- return text ;
84- }
88+ let result = text ;
8589
86- let result = text . replace ( COMPLETE_ARTIFACT_BLOCK_RE , '' ) . replace ( UNCLOSED_ARTIFACT_RE , '' ) ;
90+ if ( result . includes ( 'devonzA' ) ) {
91+ result = result . replace ( COMPLETE_ARTIFACT_BLOCK_RE , '' ) . replace ( UNCLOSED_ARTIFACT_RE , '' ) ;
92+ result = result . replace ( LEFTOVER_TAG_RE , '' ) ;
93+ }
8794
88- result = result . replace ( LEFTOVER_TAG_RE , '' ) ;
95+ if ( result . includes ( 'assis' ) || result . includes ( 'Assis' ) ) {
96+ result = result . replace ( ASSISTANT_TAG_RE , '' ) ;
97+ }
8998
9099 return result ;
91100}
0 commit comments