fix: prevent scrollbar flickering in chat view during content streaming (#6266)

This commit is contained in:
Daniel 2025-07-26 21:55:12 -05:00 committed by GitHub
parent 623fa2a025
commit 43f649b03f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 119 additions and 90 deletions

View file

@ -21,7 +21,7 @@ export const Markdown = memo(({ markdown, partial }: { markdown?: string; partia
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
style={{ position: "relative" }}>
<div style={{ wordBreak: "break-word", overflowWrap: "anywhere", marginBottom: -15, marginTop: -15 }}>
<div style={{ wordBreak: "break-word", overflowWrap: "anywhere" }}>
<MarkdownBlock markdown={markdown} />
</div>
{markdown && !partial && isHovering && (

View file

@ -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 = {
a: ({ href, children, ...props }: any) => {
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
// 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,
})
}
return (
<a {...props} href={href} onClick={handleClick}>
{children}
</a>
)
},
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 <pre>{children}</pre>
}
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")) {
const components = useMemo(
() => ({
table: ({ children, ...props }: any) => {
return (
<div style={{ margin: "1em 0" }}>
<MermaidBlock code={codeString} />
<div className="table-wrapper">
<table {...props}>{children}</table>
</div>
)
}
},
a: ({ href, children, ...props }: any) => {
const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
// Only process file:// protocol or local file paths
const isLocalPath = href?.startsWith("file://") || href?.startsWith("/") || !href?.includes("://")
// Extract language from className
const match = /language-(\w+)/.exec(className)
const language = match ? match[1] : "text"
if (!isLocalPath) {
return
}
// Wrap CodeBlock in a div to ensure proper separation
return (
<div style={{ margin: "1em 0" }}>
<CodeBlock source={codeString} language={language} />
</div>
)
},
code: ({ children, className, ...props }: any) => {
// This handles inline code
return (
<code className={className} {...props}>
{children}
</code>
)
},
}
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,
})
}
return (
<a {...props} href={href} onClick={handleClick}>
{children}
</a>
)
},
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 <pre>{children}</pre>
}
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")) {
return (
<div style={{ margin: "1em 0" }}>
<MermaidBlock code={codeString} />
</div>
)
}
// 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 (
<div style={{ margin: "1em 0" }}>
<CodeBlock source={codeString} language={language} />
</div>
)
},
code: ({ children, className, ...props }: any) => {
// This handles inline code
return (
<code className={className} {...props}>
{children}
</code>
)
},
}),
[],
)
return (
<StyledMarkdown>