diff --git a/apps/web/package.json b/apps/web/package.json index 6c16e541df..2a0c877479 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", + "remark-breaks": "^4.0.0", "require-in-the-middle": "^7.5.2", "shiki": "^3.7.0", "sonner": "^2.0.3", diff --git a/apps/web/src/app/(authenticated)/usage/Messages.tsx b/apps/web/src/app/(authenticated)/usage/Messages.tsx index ab3c8be91a..6e310fb07c 100644 --- a/apps/web/src/app/(authenticated)/usage/Messages.tsx +++ b/apps/web/src/app/(authenticated)/usage/Messages.tsx @@ -1,5 +1,6 @@ import { useMemo, useEffect, useState } from 'react'; import ReactMarkdown from 'react-markdown'; +import remarkBreaks from 'remark-breaks'; import { Link2 } from 'lucide-react'; import { toast } from 'sonner'; @@ -312,6 +313,7 @@ export const Messages = ({ ) : (
({ + useAutoScroll: () => ({ + containerRef: { current: null }, + scrollToBottom: vi.fn(), + autoScrollToBottom: vi.fn(), + userHasScrolled: false, + }), +})); + +vi.mock('@/lib/formatters', () => ({ + formatTimestamp: (timestamp: number) => new Date(timestamp).toLocaleString(), +})); + +describe('Messages Component - Newline Handling', () => { + const createMockMessage = (text: string, id = '1'): Message => ({ + id, + orgId: null, + userId: 'test-user', + taskId: 'test-task', + text, + timestamp: Date.now(), + ts: Date.now(), + type: 'say', + say: 'text', + ask: null, + mode: 'code', + reasoning: null, + partial: null, + }); + + it('should render messages with newlines properly', () => { + const messageWithNewlines = createMockMessage('Line 1\nLine 2\nLine 3'); + + render(); + + // The text should be present in the document + expect(screen.getByText(/Line 1/)).toBeInTheDocument(); + expect(screen.getByText(/Line 2/)).toBeInTheDocument(); + expect(screen.getByText(/Line 3/)).toBeInTheDocument(); + }); + + it('should handle multiple consecutive newlines', () => { + const messageWithMultipleNewlines = createMockMessage( + 'Paragraph 1\n\nParagraph 2\n\n\nParagraph 3', + ); + + render(); + + expect(screen.getByText(/Paragraph 1/)).toBeInTheDocument(); + expect(screen.getByText(/Paragraph 2/)).toBeInTheDocument(); + expect(screen.getByText(/Paragraph 3/)).toBeInTheDocument(); + }); + + it('should handle mixed content with newlines', () => { + const mixedContent = createMockMessage( + 'Regular text\n**Bold text**\n`code snippet`\nMore text', + ); + + render(); + + expect(screen.getByText(/Regular text/)).toBeInTheDocument(); + expect(screen.getByText(/Bold text/)).toBeInTheDocument(); + expect(screen.getByText(/code snippet/)).toBeInTheDocument(); + expect(screen.getByText(/More text/)).toBeInTheDocument(); + }); + + it('should handle command messages without markdown processing', () => { + const commandMessage: Message = { + id: '1', + orgId: null, + userId: 'test-user', + taskId: 'test-task', + text: 'npm install\ncd project\nls -la', + timestamp: Date.now(), + ts: Date.now(), + type: 'ask', + say: null, + ask: 'command', + mode: 'code', + reasoning: null, + partial: null, + }; + + render(); + + // Command messages should preserve newlines in the monospace container + const commandContainer = screen.getByText(/npm install/); + expect(commandContainer).toBeInTheDocument(); + expect(commandContainer.closest('.font-mono')).toBeInTheDocument(); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e17d22f110..2d1a39997a 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) + remark-breaks: + specifier: ^4.0.0 + version: 4.0.0 require-in-the-middle: specifier: ^7.5.2 version: 7.5.2 @@ -3979,6 +3982,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + eslint-config-prettier@10.1.5: resolution: {integrity: sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==} hasBin: true @@ -5006,6 +5013,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + mdast-util-from-markdown@2.0.2: resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} @@ -5018,6 +5028,9 @@ packages: mdast-util-mdxjs-esm@2.0.1: resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + mdast-util-newline-to-break@2.0.0: + resolution: {integrity: sha512-MbgeFca0hLYIEx/2zGsszCSEJJ1JSCdiY5xQxRcLDDGa8EPvlLPupJ4DSajbMPAnC0je8jfb9TiUATnxxrHUog==} + mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} @@ -5893,6 +5906,9 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + remark-breaks@4.0.0: + resolution: {integrity: sha512-IjEjJOkH4FuJvHZVIW0QCDWxcG96kCq7An/KVH2NfJe6rKZU2AsHeB3OEjPNRxi4QC34Xdx7I2KGYn6IpT7gxQ==} + remark-parse@11.0.0: resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} @@ -10593,6 +10609,8 @@ snapshots: escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-config-prettier@10.1.5(eslint@9.29.0(jiti@2.4.2)): dependencies: eslint: 9.29.0(jiti@2.4.2) @@ -11697,6 +11715,13 @@ snapshots: math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + mdast-util-from-markdown@2.0.2: dependencies: '@types/mdast': 4.0.4 @@ -11753,6 +11778,11 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-newline-to-break@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-find-and-replace: 3.0.2 + mdast-util-phrasing@4.1.0: dependencies: '@types/mdast': 4.0.4 @@ -12800,6 +12830,12 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + remark-breaks@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-newline-to-break: 2.0.0 + unified: 11.0.5 + remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4