| ... | ... | @@ -226,6 +226,17 @@ function buildUserTimelineEntry(text: string) { |
| 226 | 226 | }; |
| 227 | 227 | } |
| 228 | 228 | |
| 229 | function buildAssistantTimelineEntry(text: string) { |
| 230 | const entry = buildUserTimelineEntry(text); |
| 231 | return { |
| 232 | ...entry, |
| 233 | message: { |
| 234 | ...entry.message, |
| 235 | role: "assistant" as const, |
| 236 | }, |
| 237 | }; |
| 238 | } |
| 239 | |
| 229 | 240 | describe("MessagesTimeline", () => { |
| 230 | 241 | it("sizes expanded tool details with the configured code font size", () => { |
| 231 | 242 | expect(toolCallExpandedBodyClassName).toContain("var(--font-size-code"); |
| ... | ... | @@ -470,7 +481,142 @@ describe("MessagesTimeline", () => { |
| 470 | 481 | expect(markup).toContain("rounded-2xl bg-message p-3"); |
| 471 | 482 | }); |
| 472 | 483 | |
| 473 | | it("renders inline terminal labels with the composer chip UI", () => { |
| 484 | it("preserves arbitrary XML-like tags and comparisons in rendered user messages", async () => { |
| 485 | const { MessagesTimeline } = await import("./MessagesTimeline"); |
| 486 | const markup = renderToStaticMarkup( |
| 487 | <MessagesTimeline |
| 488 | {...buildProps()} |
| 489 | timelineEntries={[ |
| 490 | buildUserTimelineEntry( |
| 491 | [ |
| 492 | 'Without reading a file, do you have <global-agent-instructions scope="workspace">', |
| 493 | 'Before <nested data-value="a&b">inside</nested> after', |
| 494 | "</global-agent-instructions> in your context?", |
| 495 | "Comparison: 2 < 3 and 5 > 4.", |
| 496 | ].join("\n"), |
| 497 | ), |
| 498 | ]} |
| 499 | />, |
| 500 | ); |
| 501 | |
| 502 | expect(markup).toContain("&lt;global-agent-instructions scope=&quot;workspace&quot;&gt;"); |
| 503 | expect(markup).toContain( |
| 504 | "Before &lt;nested data-value=&quot;a&amp;b&quot;&gt;inside&lt;/nested&gt; after", |
| 505 | ); |
| 506 | expect(markup).toContain("&lt;/global-agent-instructions&gt; in your context?"); |
| 507 | expect(markup).toContain("Comparison: 2 &lt; 3 and 5 &gt; 4."); |
| 508 | }); |
| 509 | |
| 510 | it("preserves XML-like source inside user code spans and fences", async () => { |
| 511 | const { MessagesTimeline } = await import("./MessagesTimeline"); |
| 512 | const markup = renderToStaticMarkup( |
| 513 | <MessagesTimeline |
| 514 | {...buildProps()} |
| 515 | timelineEntries={[ |
| 516 | buildUserTimelineEntry( |
| 517 | [ |
| 518 | 'Inline `<tag attr="x">`', |
| 519 | "", |
| 520 | "```xml", |
| 521 | '<root><child enabled="true" /></root>', |
| 522 | "```", |
| 523 | ].join("\n"), |
| 524 | ), |
| 525 | ]} |
| 526 | />, |
| 527 | ); |
| 528 | |
| 529 | expect(markup).toContain('<code data-inline-code="">&lt;tag attr=&quot;x&quot;&gt;</code>'); |
| 530 | expect(markup).toContain("&lt;root&gt;&lt;child enabled=&quot;true&quot; /&gt;&lt;/root&gt;"); |
| 531 | }); |
| 532 | |
| 533 | it("does not render markdown title attributes in user messages", async () => { |
| 534 | const { MessagesTimeline } = await import("./MessagesTimeline"); |
| 535 | const markup = renderToStaticMarkup( |
| 536 | <MessagesTimeline |
| 537 | {...buildProps()} |
| 538 | timelineEntries={[ |
| 539 | buildUserTimelineEntry( |
| 540 | '[link](https://example.com "link tip") ', |
| 541 | ), |
| 542 | ]} |
| 543 | />, |
| 544 | ); |
| 545 | |
| 546 | expect(markup).toContain('href="https://example.com"'); |
| 547 | expect(markup).toContain('src="https://example.com/image.png"'); |
| 548 | expect(markup).not.toContain('title="link tip"'); |
| 549 | expect(markup).not.toContain('title="image tip"'); |
| 550 | }); |
| 551 | |
| 552 | it("renders unsafe user HTML as inert source text", async () => { |
| 553 | const { MessagesTimeline } = await import("./MessagesTimeline"); |
| 554 | const markup = renderToStaticMarkup( |
| 555 | <MessagesTimeline |
| 556 | {...buildProps()} |
| 557 | timelineEntries={[ |
| 558 | buildUserTimelineEntry( |
| 559 | '<script>globalThis.__t3Xss = 1</script><img src="x" onerror="globalThis.__t3Xss = 2">', |
| 560 | ), |
| 561 | ]} |
| 562 | />, |
| 563 | ); |
| 564 | |
| 565 | expect(markup).toContain("&lt;script&gt;globalThis.__t3Xss = 1&lt;/script&gt;"); |
| 566 | expect(markup).toContain( |
| 567 | "&lt;img src=&quot;x&quot; onerror=&quot;globalThis.__t3Xss = 2&quot;&gt;", |
| 568 | ); |
| 569 | expect(markup).not.toMatch(/<script(?:\s|>)/i); |
| 570 | expect(markup).not.toMatch(/<img(?:\s|>)/i); |
| 571 | }); |
| 572 | |
| 573 | it("continues to render sanitized raw HTML in assistant messages", async () => { |
| 574 | const { MessagesTimeline } = await import("./MessagesTimeline"); |
| 575 | const markup = renderToStaticMarkup( |
| 576 | <MessagesTimeline |
| 577 | {...buildProps()} |
| 578 | timelineEntries={[ |
| 579 | buildAssistantTimelineEntry("<details><summary>More</summary>Details</details>"), |
| 580 | ]} |
| 581 | />, |
| 582 | ); |
| 583 | |
| 584 | expect(markup).toContain('data-markdown-details=""'); |
| 585 | expect(markup).toContain("More"); |
| 586 | expect(markup).not.toContain("&lt;details&gt;"); |
| 587 | }); |
| 588 | |
| 589 | it("sanitizes executable HTML while preserving supported assistant markup", async () => { |
| 590 | const { MessagesTimeline } = await import("./MessagesTimeline"); |
| 591 | const markup = renderToStaticMarkup( |
| 592 | <MessagesTimeline |
| 593 | {...buildProps()} |
| 594 | timelineEntries={[ |
| 595 | buildAssistantTimelineEntry( |
| 596 | [ |
| 597 | '<details open onclick="globalThis.__t3Xss = 1">', |
| 598 | "<summary>Safe details</summary>", |
| 599 | "<script>globalThis.__t3Xss = 2</script>", |
| 600 | '<img src="x" onerror="globalThis.__t3Xss = 3">', |
| 601 | '<a href="javascript:globalThis.__t3Xss = 4">Unsafe link</a>', |
| 602 | "</details>", |
| 603 | ].join(""), |
| 604 | ), |
| 605 | ]} |
| 606 | />, |
| 607 | ); |
| 608 | |
| 609 | expect(markup).toContain('data-markdown-details=""'); |
| 610 | expect(markup).toContain("Safe details"); |
| 611 | expect(markup).not.toMatch(/<script(?:\s|>)/i); |
| 612 | expect(markup).not.toContain("onclick="); |
| 613 | expect(markup).not.toContain("onerror="); |
| 614 | expect(markup).not.toContain("javascript:"); |
| 615 | expect(markup).not.toContain("globalThis.__t3Xss"); |
| 616 | }); |
| 617 | |
| 618 | it("renders inline terminal labels with the composer chip UI", async () => { |
| 619 | const { MessagesTimeline } = await import("./MessagesTimeline"); |
| 474 | 620 | const markup = renderToStaticMarkup( |
| 475 | 621 | <MessagesTimeline |
| 476 | 622 | {...buildProps()} |