From 0156265963b31ae662415849189b4facddc24e73 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Wed, 2 Jul 2025 11:26:33 -0400 Subject: [PATCH] Render markdown in questions (#184) --- apps/web/package.json | 1 + .../app/(authenticated)/usage/Messages.tsx | 15 ++++++- .../components/ui/__tests__/Messages.test.tsx | 41 +++++++++++++++++++ pnpm-lock.yaml | 20 +++++++++ 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/apps/web/package.json b/apps/web/package.json index 2a0c877479..bb9eb98a8c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -63,6 +63,7 @@ "react-markdown": "^10.1.0", "react-use": "^17.6.0", "recharts": "^2.15.3", + "rehype-sanitize": "^6.0.0", "remark-breaks": "^4.0.0", "require-in-the-middle": "^7.5.2", "shiki": "^3.7.0", diff --git a/apps/web/src/app/(authenticated)/usage/Messages.tsx b/apps/web/src/app/(authenticated)/usage/Messages.tsx index 6e310fb07c..1ed2006106 100644 --- a/apps/web/src/app/(authenticated)/usage/Messages.tsx +++ b/apps/web/src/app/(authenticated)/usage/Messages.tsx @@ -1,6 +1,7 @@ import { useMemo, useEffect, useState } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkBreaks from 'remark-breaks'; +import rehypeSanitize from 'rehype-sanitize'; import { Link2 } from 'lucide-react'; import { toast } from 'sonner'; @@ -284,8 +285,17 @@ export const Messages = ({ {isQuestion && questionData ? (
{questionData.question && ( -
- {questionData.question} +
+ + {questionData.question} +
)} {questionData.suggestions && @@ -314,6 +324,7 @@ export const Messages = ({
{ expect(commandContainer).toBeInTheDocument(); expect(commandContainer.closest('.font-mono')).toBeInTheDocument(); }); + + it('should handle question data with newlines in the question text', () => { + const questionMessage: Message = { + id: '1', + orgId: null, + userId: 'test-user', + taskId: 'test-task', + text: JSON.stringify({ + question: + 'What would you like to do?\nPlease choose from the options below:\n\n1. Option A\n2. Option B', + suggest: ['Option A', 'Option B'], + }), + timestamp: Date.now(), + ts: Date.now(), + type: 'ask', + say: null, + ask: 'followup', + mode: 'code', + reasoning: null, + partial: null, + }; + + render(); + + // Question text should be present and properly formatted + expect(screen.getByText(/What would you like to do/)).toBeInTheDocument(); + expect( + screen.getByText(/Please choose from the options below/), + ).toBeInTheDocument(); + + // Should find both the list items and suggestion buttons (multiple instances expected) + const optionAElements = screen.getAllByText(/Option A/); + const optionBElements = screen.getAllByText(/Option B/); + + // Should have at least 2 instances of each option (one in markdown list, one in suggestion button) + expect(optionAElements.length).toBeGreaterThanOrEqual(2); + expect(optionBElements.length).toBeGreaterThanOrEqual(2); + + // Verify the markdown rendered the numbered list properly + expect(screen.getByRole('list')).toBeInTheDocument(); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d1a39997a..bacb36f329 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -299,6 +299,9 @@ importers: recharts: specifier: ^2.15.3 version: 2.15.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + rehype-sanitize: + specifier: ^6.0.0 + version: 6.0.0 remark-breaks: specifier: ^4.0.0 version: 4.0.0 @@ -4376,6 +4379,9 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-sanitize@5.0.2: + resolution: {integrity: sha512-3yTWghByc50aGS7JlGhk61SPenfE/p1oaFeNwkOOyrscaOkMGrcW9+Cy/QAIOBpZxP1yqDIzFMR0+Np0i0+usg==} + hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} @@ -5906,6 +5912,9 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + rehype-sanitize@6.0.0: + resolution: {integrity: sha512-CsnhKNsyI8Tub6L4sm5ZFsme4puGfc6pYylvXo1AeqaGbjOYyzNv3qZPwvs0oMJ39eryyeOdmxwUIo94IpEhqg==} + remark-breaks@4.0.0: resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==} @@ -11064,6 +11073,12 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-sanitize@5.0.2: + dependencies: + '@types/hast': 3.0.4 + '@ungap/structured-clone': 1.3.0 + unist-util-position: 5.0.0 + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.4 @@ -12830,6 +12845,11 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + rehype-sanitize@6.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-sanitize: 5.0.2 + remark-breaks@4.0.0: dependencies: '@types/mdast': 4.0.4