diff --git a/client/src/components/Chat/Messages/Content/Markdown.tsx b/client/src/components/Chat/Messages/Content/Markdown.tsx index 1217869a2c..4daef8f80b 100644 --- a/client/src/components/Chat/Messages/Content/Markdown.tsx +++ b/client/src/components/Chat/Messages/Content/Markdown.tsx @@ -19,7 +19,7 @@ import { ArtifactProvider, CodeBlockProvider } from '~/Providers'; import MarkdownErrorBoundary from './MarkdownErrorBoundary'; import { langSubset, preprocessLaTeX } from '~/utils'; import { unicodeCitation } from '~/components/Web'; -import { code, a, p, img } from './MarkdownComponents'; +import { code, a, p, img, table } from './MarkdownComponents'; import store from '~/store'; type TContentProps = { @@ -88,6 +88,7 @@ const Markdown = memo(function Markdown({ content = '', isLatestMessage }: TCont a, p, img, + table, artifact: Artifact, citation: Citation, 'highlighted-text': HighlightedText, diff --git a/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx b/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx index c38c40f1e1..5147dd2a9a 100644 --- a/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx +++ b/client/src/components/Chat/Messages/Content/MarkdownComponents.tsx @@ -185,6 +185,19 @@ export const p: React.ElementType = memo(function MarkdownParagraph({ children } }); p.displayName = 'MarkdownParagraph'; +type TTableProps = { + children: React.ReactNode; +}; + +export const table: React.ElementType = memo(function MarkdownTable({ children }: TTableProps) { + return ( +
+ {children}
+
+ ); +}); +table.displayName = 'MarkdownTable'; + type TImageProps = { src?: string; alt?: string; diff --git a/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx b/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx index 0342c60f8a..9862da2102 100644 --- a/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx +++ b/client/src/components/Chat/Messages/Content/MarkdownErrorBoundary.tsx @@ -4,7 +4,7 @@ import supersub from 'remark-supersub'; import ReactMarkdown from 'react-markdown'; import rehypeHighlight from 'rehype-highlight'; import type { PluggableList } from 'unified'; -import { code, codeNoExecution, a, p } from './MarkdownComponents'; +import { code, codeNoExecution, a, p, table } from './MarkdownComponents'; import { CodeBlockProvider } from '~/Providers'; import { langSubset } from '~/utils'; @@ -72,6 +72,7 @@ class MarkdownErrorBoundary extends React.Component< code: codeExecution ? code : codeNoExecution, a, p, + table, } as { [nodeType: string]: React.ElementType; } diff --git a/client/src/components/Chat/Messages/Content/MarkdownLite.tsx b/client/src/components/Chat/Messages/Content/MarkdownLite.tsx index 24980d8a90..44656ae77f 100644 --- a/client/src/components/Chat/Messages/Content/MarkdownLite.tsx +++ b/client/src/components/Chat/Messages/Content/MarkdownLite.tsx @@ -6,7 +6,7 @@ import supersub from 'remark-supersub'; import ReactMarkdown from 'react-markdown'; import rehypeHighlight from 'rehype-highlight'; import type { PluggableList } from 'unified'; -import { code, codeNoExecution, a, p, img } from './MarkdownComponents'; +import { code, codeNoExecution, a, p, img, table } from './MarkdownComponents'; import { CodeBlockProvider, ArtifactProvider } from '~/Providers'; import MarkdownErrorBoundary from './MarkdownErrorBoundary'; import { langSubset } from '~/utils'; @@ -44,6 +44,7 @@ const MarkdownLite = memo( a, p, img, + table, } as { [nodeType: string]: React.ElementType; } diff --git a/client/src/components/Chat/Messages/Content/__tests__/Markdown.mcpui.test.tsx b/client/src/components/Chat/Messages/Content/__tests__/Markdown.mcpui.test.tsx index 6ca06056fa..bde9614904 100644 --- a/client/src/components/Chat/Messages/Content/__tests__/Markdown.mcpui.test.tsx +++ b/client/src/components/Chat/Messages/Content/__tests__/Markdown.mcpui.test.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; import Markdown from '../Markdown'; +import MarkdownLite from '../MarkdownLite'; import { RecoilRoot } from 'recoil'; import { UI_RESOURCE_MARKER } from '~/components/MCPUIResource/plugin'; import { @@ -108,3 +109,35 @@ describe('Markdown with MCP UI markers (resource IDs)', () => { expect(renderers[1]).toHaveAttribute('data-resource-uri', 'ui://weather/nyc'); }); }); + +describe('Markdown table rendering', () => { + const tableMarkdown = [ + '| Alpha | Bravo | Charlie | Delta | Echo | Foxtrot | Golf | Hotel |', + '| --- | --- | --- | --- | --- | --- | --- | --- |', + '| one | two | three | four | five | six | seven | eight |', + ].join('\n'); + + it('wraps GFM tables in a horizontally scrollable container', () => { + render( + + + , + ); + + expect(screen.getByRole('table').parentElement).toHaveClass( + 'markdown-table-wrapper', + 'w-full', + 'max-w-full', + ); + }); + + it('wraps lightweight Markdown tables in a horizontally scrollable container', () => { + render(); + + expect(screen.getByRole('table').parentElement).toHaveClass( + 'markdown-table-wrapper', + 'w-full', + 'max-w-full', + ); + }); +}); diff --git a/client/src/style.css b/client/src/style.css index 902860ef52..35dee56a1d 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -1496,6 +1496,27 @@ button { background-color: transparent; } +.markdown-table-wrapper { + -webkit-overflow-scrolling: touch; + overflow-x: auto; + overscroll-behavior-x: contain; + padding-bottom: 0.25rem; + scrollbar-color: var(--border-medium) transparent; + scrollbar-width: thin; +} + +.markdown-table-wrapper::-webkit-scrollbar { + height: 0.5rem; +} + +.markdown-table-wrapper::-webkit-scrollbar-thumb { + background-color: var(--border-medium); +} + +.markdown-table-wrapper::-webkit-scrollbar-thumb:hover { + background-color: var(--border-heavy); +} + /* Show scrollbar only on hover */ .scrollbar-hover { scrollbar-width: thin;