diff --git a/webview-ui/src/components/chat/Markdown.tsx b/webview-ui/src/components/chat/Markdown.tsx
index ba838284d7d..87780d5df8d 100644
--- a/webview-ui/src/components/chat/Markdown.tsx
+++ b/webview-ui/src/components/chat/Markdown.tsx
@@ -21,7 +21,7 @@ export const Markdown = memo(({ markdown, partial }: { markdown?: string; partia
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
style={{ position: "relative" }}>
-
+
{markdown && !partial && isHovering && (
diff --git a/webview-ui/src/components/common/MarkdownBlock.tsx b/webview-ui/src/components/common/MarkdownBlock.tsx
index ca6c9316b10..cae609d9553 100644
--- a/webview-ui/src/components/common/MarkdownBlock.tsx
+++ b/webview-ui/src/components/common/MarkdownBlock.tsx
@@ -1,4 +1,4 @@
-import React, { memo } from "react"
+import React, { memo, useMemo } from "react"
import ReactMarkdown from "react-markdown"
import styled from "styled-components"
import { visit } from "unist-util-visit"
@@ -7,7 +7,6 @@ import remarkMath from "remark-math"
import remarkGfm from "remark-gfm"
import { vscode } from "@src/utils/vscode"
-import { useExtensionState } from "@src/context/ExtensionStateContext"
import CodeBlock from "./CodeBlock"
import MermaidBlock from "./MermaidBlock"
@@ -117,6 +116,19 @@ const StyledMarkdown = styled.div`
p {
white-space: pre-wrap;
+ margin: 0.5em 0;
+ }
+
+ /* Prevent layout shifts during streaming */
+ pre {
+ min-height: 3em;
+ transition: height 0.2s ease-out;
+ }
+
+ /* Code block container styling */
+ div:has(> pre) {
+ position: relative;
+ contain: layout style;
}
a {
@@ -133,11 +145,18 @@ const StyledMarkdown = styled.div`
/* Table styles for remark-gfm */
table {
- width: 100%;
border-collapse: collapse;
margin: 1em 0;
+ width: auto;
+ min-width: 50%;
+ max-width: 100%;
+ table-layout: fixed;
+ }
+
+ /* Table wrapper for horizontal scrolling */
+ .table-wrapper {
overflow-x: auto;
- display: block;
+ margin: 1em 0;
}
th,
@@ -145,6 +164,8 @@ const StyledMarkdown = styled.div`
border: 1px solid var(--vscode-panel-border);
padding: 8px 12px;
text-align: left;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
}
th {
@@ -163,96 +184,104 @@ const StyledMarkdown = styled.div`
`
const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
- const { theme: _theme } = useExtensionState()
+ const components = useMemo(
+ () => ({
+ table: ({ children, ...props }: any) => {
+ return (
+
+ )
+ },
+ a: ({ href, children, ...props }: any) => {
+ const handleClick = (e: React.MouseEvent
) => {
+ // Only process file:// protocol or local file paths
+ const isLocalPath = href?.startsWith("file://") || href?.startsWith("/") || !href?.includes("://")
+
+ if (!isLocalPath) {
+ return
+ }
+
+ e.preventDefault()
+
+ // Handle absolute vs project-relative paths
+ let filePath = href.replace("file://", "")
+
+ // Extract line number if present
+ const match = filePath.match(/(.*):(\d+)(-\d+)?$/)
+ let values = undefined
+ if (match) {
+ filePath = match[1]
+ values = { line: parseInt(match[2]) }
+ }
+
+ // Add ./ prefix if needed
+ if (!filePath.startsWith("/") && !filePath.startsWith("./")) {
+ filePath = "./" + filePath
+ }
+
+ vscode.postMessage({
+ type: "openFile",
+ text: filePath,
+ values,
+ })
+ }
- const components = {
- a: ({ href, children, ...props }: any) => {
- const handleClick = (e: React.MouseEvent) => {
- // Only process file:// protocol or local file paths
- const isLocalPath = href?.startsWith("file://") || href?.startsWith("/") || !href?.includes("://")
+ return (
+
+ {children}
+
+ )
+ },
+ pre: ({ children, ..._props }: any) => {
+ // The structure from react-markdown v9 is: pre > code > text
+ const codeEl = children as React.ReactElement
- if (!isLocalPath) {
- return
+ if (!codeEl || !codeEl.props) {
+ return {children}
}
- e.preventDefault()
-
- // Handle absolute vs project-relative paths
- let filePath = href.replace("file://", "")
+ const { className = "", children: codeChildren } = codeEl.props
- // Extract line number if present
- const match = filePath.match(/(.*):(\d+)(-\d+)?$/)
- let values = undefined
- if (match) {
- filePath = match[1]
- values = { line: parseInt(match[2]) }
+ // Get the actual code text
+ let codeString = ""
+ if (typeof codeChildren === "string") {
+ codeString = codeChildren
+ } else if (Array.isArray(codeChildren)) {
+ codeString = codeChildren.filter((child) => typeof child === "string").join("")
}
- // Add ./ prefix if needed
- if (!filePath.startsWith("/") && !filePath.startsWith("./")) {
- filePath = "./" + filePath
+ // Handle mermaid diagrams
+ if (className.includes("language-mermaid")) {
+ return (
+
+
+
+ )
}
- vscode.postMessage({
- type: "openFile",
- text: filePath,
- values,
- })
- }
-
- return (
-
- {children}
-
- )
- },
- pre: ({ children, ..._props }: any) => {
- // The structure from react-markdown v9 is: pre > code > text
- const codeEl = children as React.ReactElement
-
- if (!codeEl || !codeEl.props) {
- return {children}
- }
-
- const { className = "", children: codeChildren } = codeEl.props
-
- // Get the actual code text
- let codeString = ""
- if (typeof codeChildren === "string") {
- codeString = codeChildren
- } else if (Array.isArray(codeChildren)) {
- codeString = codeChildren.filter((child) => typeof child === "string").join("")
- }
-
- // Handle mermaid diagrams
- if (className.includes("language-mermaid")) {
+ // Extract language from className
+ const match = /language-(\w+)/.exec(className)
+ const language = match ? match[1] : "text"
+
+ // Wrap CodeBlock in a div to ensure proper separation
return (
-
+
)
- }
-
- // Extract language from className
- const match = /language-(\w+)/.exec(className)
- const language = match ? match[1] : "text"
-
- // Wrap CodeBlock in a div to ensure proper separation
- return (
-
-
-
- )
- },
- code: ({ children, className, ...props }: any) => {
- // This handles inline code
- return (
-
- {children}
-
- )
- },
- }
+ },
+ code: ({ children, className, ...props }: any) => {
+ // This handles inline code
+ return (
+
+ {children}
+
+ )
+ },
+ }),
+ [],
+ )
return (