Add remark_breaks to handle newlines (#183)

This commit is contained in:
Matt Rubens 2025-07-02 11:10:15 -04:00 committed by GitHub
parent 34cfd156d1
commit 2d8211aea7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 136 additions and 0 deletions

View file

@ -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",

View file

@ -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 = ({
) : (
<div className="text-sm leading-relaxed markdown-prose">
<ReactMarkdown
remarkPlugins={[remarkBreaks]}
components={{
a: PlainTextLink,
code: CodeBlock,

View file

@ -0,0 +1,97 @@
import { render, screen } from '@testing-library/react';
import { vi, describe, it, expect } from 'vitest';
import { Messages } from '@/app/(authenticated)/usage/Messages';
import type { Message } from '@/actions/analytics/messages';
// Mock the hooks and dependencies
vi.mock('@/hooks/useAutoScroll', () => ({
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(<Messages messages={[messageWithNewlines]} />);
// 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(<Messages messages={[messageWithMultipleNewlines]} />);
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(<Messages messages={[mixedContent]} />);
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(<Messages messages={[commandMessage]} />);
// Command messages should preserve newlines in the monospace container
const commandContainer = screen.getByText(/npm install/);
expect(commandContainer).toBeInTheDocument();
expect(commandContainer.closest('.font-mono')).toBeInTheDocument();
});
});

36
pnpm-lock.yaml generated
View file

@ -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