@@ -225,6 +225,17 @@ function buildUserTimelineEntry(text: string) {
225225 } ;
226226}
227227
228+ function buildAssistantTimelineEntry ( text : string ) {
229+ const entry = buildUserTimelineEntry ( text ) ;
230+ return {
231+ ...entry ,
232+ message : {
233+ ...entry . message ,
234+ role : "assistant" as const ,
235+ } ,
236+ } ;
237+ }
238+
228239describe ( "MessagesTimeline" , ( ) => {
229240 it ( "uses the larger leading inset only when the top fade is enabled" , ( ) => {
230241 const timelineEntries = [ buildUserTimelineEntry ( "Hello" ) ] ;
@@ -464,7 +475,123 @@ describe("MessagesTimeline", () => {
464475 expect ( markup ) . toContain ( "rounded-2xl bg-message p-3" ) ;
465476 } ) ;
466477
467- it ( "renders inline terminal labels with the composer chip UI" , ( ) => {
478+ it ( "preserves arbitrary XML-like tags and comparisons in rendered user messages" , async ( ) => {
479+ const { MessagesTimeline } = await import ( "./MessagesTimeline" ) ;
480+ const markup = renderToStaticMarkup (
481+ < MessagesTimeline
482+ { ...buildProps ( ) }
483+ timelineEntries = { [
484+ buildUserTimelineEntry (
485+ [
486+ 'Without reading a file, do you have <global-agent-instructions scope="workspace">' ,
487+ 'Before <nested data-value="a&b">inside</nested> after' ,
488+ "</global-agent-instructions> in your context?" ,
489+ "Comparison: 2 < 3 and 5 > 4." ,
490+ ] . join ( "\n" ) ,
491+ ) ,
492+ ] }
493+ /> ,
494+ ) ;
495+
496+ expect ( markup ) . toContain ( "<global-agent-instructions scope="workspace">" ) ;
497+ expect ( markup ) . toContain (
498+ "Before <nested data-value="a&b">inside</nested> after" ,
499+ ) ;
500+ expect ( markup ) . toContain ( "</global-agent-instructions> in your context?" ) ;
501+ expect ( markup ) . toContain ( "Comparison: 2 < 3 and 5 > 4." ) ;
502+ } ) ;
503+
504+ it ( "preserves XML-like source inside user code spans and fences" , async ( ) => {
505+ const { MessagesTimeline } = await import ( "./MessagesTimeline" ) ;
506+ const markup = renderToStaticMarkup (
507+ < MessagesTimeline
508+ { ...buildProps ( ) }
509+ timelineEntries = { [
510+ buildUserTimelineEntry (
511+ [
512+ 'Inline `<tag attr="x">`' ,
513+ "" ,
514+ "```xml" ,
515+ '<root><child enabled="true" /></root>' ,
516+ "```" ,
517+ ] . join ( "\n" ) ,
518+ ) ,
519+ ] }
520+ /> ,
521+ ) ;
522+
523+ expect ( markup ) . toContain ( '<code data-inline-code=""><tag attr="x"></code>' ) ;
524+ expect ( markup ) . toContain ( "<root><child enabled="true" /></root>" ) ;
525+ } ) ;
526+
527+ it ( "renders unsafe user HTML as inert source text" , async ( ) => {
528+ const { MessagesTimeline } = await import ( "./MessagesTimeline" ) ;
529+ const markup = renderToStaticMarkup (
530+ < MessagesTimeline
531+ { ...buildProps ( ) }
532+ timelineEntries = { [
533+ buildUserTimelineEntry (
534+ '<script>globalThis.__t3Xss = 1</script><img src="x" onerror="globalThis.__t3Xss = 2">' ,
535+ ) ,
536+ ] }
537+ /> ,
538+ ) ;
539+
540+ expect ( markup ) . toContain ( "<script>globalThis.__t3Xss = 1</script>" ) ;
541+ expect ( markup ) . toContain (
542+ "<img src="x" onerror="globalThis.__t3Xss = 2">" ,
543+ ) ;
544+ expect ( markup ) . not . toMatch ( / < s c r i p t (?: \s | > ) / i) ;
545+ expect ( markup ) . not . toMatch ( / < i m g (?: \s | > ) / i) ;
546+ } ) ;
547+
548+ it ( "continues to render sanitized raw HTML in assistant messages" , async ( ) => {
549+ const { MessagesTimeline } = await import ( "./MessagesTimeline" ) ;
550+ const markup = renderToStaticMarkup (
551+ < MessagesTimeline
552+ { ...buildProps ( ) }
553+ timelineEntries = { [
554+ buildAssistantTimelineEntry ( "<details><summary>More</summary>Details</details>" ) ,
555+ ] }
556+ /> ,
557+ ) ;
558+
559+ expect ( markup ) . toContain ( 'data-markdown-details=""' ) ;
560+ expect ( markup ) . toContain ( "More" ) ;
561+ expect ( markup ) . not . toContain ( "<details>" ) ;
562+ } ) ;
563+
564+ it ( "sanitizes executable HTML while preserving supported assistant markup" , async ( ) => {
565+ const { MessagesTimeline } = await import ( "./MessagesTimeline" ) ;
566+ const markup = renderToStaticMarkup (
567+ < MessagesTimeline
568+ { ...buildProps ( ) }
569+ timelineEntries = { [
570+ buildAssistantTimelineEntry (
571+ [
572+ '<details open onclick="globalThis.__t3Xss = 1">' ,
573+ "<summary>Safe details</summary>" ,
574+ "<script>globalThis.__t3Xss = 2</script>" ,
575+ '<img src="x" onerror="globalThis.__t3Xss = 3">' ,
576+ '<a href="javascript:globalThis.__t3Xss = 4">Unsafe link</a>' ,
577+ "</details>" ,
578+ ] . join ( "" ) ,
579+ ) ,
580+ ] }
581+ /> ,
582+ ) ;
583+
584+ expect ( markup ) . toContain ( 'data-markdown-details=""' ) ;
585+ expect ( markup ) . toContain ( "Safe details" ) ;
586+ expect ( markup ) . not . toMatch ( / < s c r i p t (?: \s | > ) / i) ;
587+ expect ( markup ) . not . toContain ( "onclick=" ) ;
588+ expect ( markup ) . not . toContain ( "onerror=" ) ;
589+ expect ( markup ) . not . toContain ( "javascript:" ) ;
590+ expect ( markup ) . not . toContain ( "globalThis.__t3Xss" ) ;
591+ } ) ;
592+
593+ it ( "renders inline terminal labels with the composer chip UI" , async ( ) => {
594+ const { MessagesTimeline } = await import ( "./MessagesTimeline" ) ;
468595 const markup = renderToStaticMarkup (
469596 < MessagesTimeline
470597 { ...buildProps ( ) }
0 commit comments