mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 20:31:37 -05:00
Replace react markdown with react remark for better performance when streaming
This commit is contained in:
1098
webview-ui/package-lock.json
generated
1098
webview-ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,6 @@
|
|||||||
"fuse.js": "^7.0.0",
|
"fuse.js": "^7.0.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-markdown": "^9.0.1",
|
|
||||||
"react-remark": "^2.1.0",
|
"react-remark": "^2.1.0",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"react-textarea-autosize": "^8.5.3",
|
"react-textarea-autosize": "^8.5.3",
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
import { VSCodeBadge, VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
|
||||||
import deepEqual from "fast-deep-equal"
|
import deepEqual from "fast-deep-equal"
|
||||||
import React, { memo, useMemo } from "react"
|
import React, { memo, useMemo } from "react"
|
||||||
import ReactMarkdown from "react-markdown"
|
|
||||||
import { ClaudeApiReqInfo, ClaudeMessage, ClaudeSayTool } from "../../../../src/shared/ExtensionMessage"
|
import { ClaudeApiReqInfo, ClaudeMessage, ClaudeSayTool } from "../../../../src/shared/ExtensionMessage"
|
||||||
import { COMMAND_OUTPUT_STRING } from "../../../../src/shared/combineCommandSequences"
|
import { COMMAND_OUTPUT_STRING } from "../../../../src/shared/combineCommandSequences"
|
||||||
import { vscode } from "../../utils/vscode"
|
import { vscode } from "../../utils/vscode"
|
||||||
import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
|
import CodeAccordian, { removeLeadingNonAlphanumeric } from "../common/CodeAccordian"
|
||||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "../common/CodeBlock"
|
||||||
import { highlightMentions } from "./TaskHeader"
|
import MarkdownBlock from "../common/MarkdownBlock"
|
||||||
import Thumbnails from "../common/Thumbnails"
|
import Thumbnails from "../common/Thumbnails"
|
||||||
|
import { highlightMentions } from "./TaskHeader"
|
||||||
|
|
||||||
interface ChatRowProps {
|
interface ChatRowProps {
|
||||||
message: ClaudeMessage
|
message: ClaudeMessage
|
||||||
@@ -764,116 +764,9 @@ const ProgressIndicator = () => (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const Markdown = memo(({ markdown }: { markdown?: string }) => {
|
const Markdown = memo(({ markdown }: { markdown?: string }) => {
|
||||||
// react-markdown lets us customize elements, so here we're using their example of replacing code blocks with SyntaxHighlighter. However when there are no language matches (` or ``` without a language specifier) then we default to a normal code element for inline code. Code blocks without a language specifier shouldn't be a common occurrence as we prompt Claude to always use a language specifier.
|
|
||||||
// when claude wraps text in thinking tags, he doesnt use line breaks so we need to insert those ourselves to render markdown correctly
|
|
||||||
// const parsed = markdown?.replace(/<thinking>([\s\S]*?)<\/thinking>/g, (match, content) => {
|
|
||||||
// return content
|
|
||||||
// // return `_<thinking>_\n\n${content}\n\n_</thinking>_`
|
|
||||||
// })
|
|
||||||
const parsed = markdown
|
|
||||||
return (
|
return (
|
||||||
<div style={{ wordBreak: "break-word", overflowWrap: "anywhere", marginBottom: -10, marginTop: -10 }}>
|
<div style={{ wordBreak: "break-word", overflowWrap: "anywhere", marginBottom: -15, marginTop: -15 }}>
|
||||||
<ReactMarkdown
|
<MarkdownBlock markdown={markdown} />
|
||||||
children={parsed}
|
|
||||||
components={{
|
|
||||||
p(props) {
|
|
||||||
const { style, ...rest } = props
|
|
||||||
return (
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
margin: 0,
|
|
||||||
marginTop: 10,
|
|
||||||
marginBottom: 10,
|
|
||||||
whiteSpace: "pre-wrap",
|
|
||||||
wordBreak: "break-word",
|
|
||||||
overflowWrap: "anywhere",
|
|
||||||
}}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
ol(props) {
|
|
||||||
const { style, ...rest } = props
|
|
||||||
return (
|
|
||||||
<ol
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
padding: "0 0 0 20px",
|
|
||||||
margin: "10px 0",
|
|
||||||
wordBreak: "break-word",
|
|
||||||
overflowWrap: "anywhere",
|
|
||||||
}}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
ul(props) {
|
|
||||||
const { style, ...rest } = props
|
|
||||||
return (
|
|
||||||
<ul
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
padding: "0 0 0 20px",
|
|
||||||
margin: "10px 0",
|
|
||||||
wordBreak: "break-word",
|
|
||||||
overflowWrap: "anywhere",
|
|
||||||
}}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
// pre always surrounds a code, and we custom handle code blocks below. Pre has some non-10 margin, while all other elements in markdown have a 10 top/bottom margin and the outer div has a -10 top/bottom margin to counteract this between chat rows. However we render markdown in a completion_result row so make sure to add padding as necessary when used within other rows.
|
|
||||||
pre(props) {
|
|
||||||
const { style, ...rest } = props
|
|
||||||
return (
|
|
||||||
<pre
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
marginTop: 10,
|
|
||||||
marginBlock: 10,
|
|
||||||
}}
|
|
||||||
{...rest}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
// https://github.com/remarkjs/react-markdown?tab=readme-ov-file#use-custom-components-syntax-highlight
|
|
||||||
code(props) {
|
|
||||||
const { children, className, node, ...rest } = props
|
|
||||||
const match = /language-(\w+)/.exec(className || "")
|
|
||||||
return match ? (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
borderRadius: 3,
|
|
||||||
border: "1px solid var(--vscode-editorGroup-border)",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}>
|
|
||||||
<CodeBlock
|
|
||||||
source={`${"```"}${match[1]}\n${String(children).replace(/\n$/, "")}\n${"```"}`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<code
|
|
||||||
{...rest}
|
|
||||||
className={className}
|
|
||||||
style={{
|
|
||||||
whiteSpace: "pre-line",
|
|
||||||
wordBreak: "break-word",
|
|
||||||
overflowWrap: "anywhere",
|
|
||||||
backgroundColor: "var(--vscode-textCodeBlock-background)",
|
|
||||||
color: "var(--vscode-textPreformat-foreground)",
|
|
||||||
fontFamily: "var(--vscode-editor-font-family)",
|
|
||||||
fontSize: "var(--vscode-editor-font-size)",
|
|
||||||
borderRadius: "3px",
|
|
||||||
border: "1px solid var(--vscode-textSeparator-foreground)",
|
|
||||||
padding: "0px 2px",
|
|
||||||
}}>
|
|
||||||
{children}
|
|
||||||
</code>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
129
webview-ui/src/components/common/MarkdownBlock.tsx
Normal file
129
webview-ui/src/components/common/MarkdownBlock.tsx
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import { memo, useEffect } from "react"
|
||||||
|
import { useRemark } from "react-remark"
|
||||||
|
import rehypeHighlight, { Options } from "rehype-highlight"
|
||||||
|
import styled from "styled-components"
|
||||||
|
import { visit } from "unist-util-visit"
|
||||||
|
import { useExtensionState } from "../../context/ExtensionStateContext"
|
||||||
|
import { CODE_BLOCK_BG_COLOR } from "./CodeBlock"
|
||||||
|
|
||||||
|
interface MarkdownBlockProps {
|
||||||
|
markdown?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledMarkdown = styled.div`
|
||||||
|
pre {
|
||||||
|
background-color: ${CODE_BLOCK_BG_COLOR};
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 13x 0;
|
||||||
|
padding: 10px 10px;
|
||||||
|
max-width: calc(100vw - 20px);
|
||||||
|
overflow-x: scroll;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > code {
|
||||||
|
.hljs-deletion {
|
||||||
|
background-color: var(--vscode-diffEditor-removedTextBackground);
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.hljs-addition {
|
||||||
|
background-color: var(--vscode-diffEditor-insertedTextBackground);
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
span.line:empty {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
word-wrap: break-word;
|
||||||
|
border-radius: 3px;
|
||||||
|
background-color: ${CODE_BLOCK_BG_COLOR};
|
||||||
|
font-size: var(--vscode-editor-font-size, var(--vscode-font-size, 12px));
|
||||||
|
font-family: var(--vscode-editor-font-family);
|
||||||
|
}
|
||||||
|
|
||||||
|
code:not(pre > code) {
|
||||||
|
font-family: var(--vscode-editor-font-family, monospace);
|
||||||
|
color: var(--vscode-textPreformat-foreground, #f78383);
|
||||||
|
background-color: var(--vscode-textCodeBlock-background, #1e1e1e);
|
||||||
|
padding: 0px 2px;
|
||||||
|
border-radius: 3px;
|
||||||
|
border: 1px solid var(--vscode-textSeparator-foreground, #424242);
|
||||||
|
white-space: pre-line;
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
font-family: var(--vscode-font-family), system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
|
||||||
|
Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||||
|
font-size: var(--vscode-font-size, 13px);
|
||||||
|
|
||||||
|
p,
|
||||||
|
li,
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const StyledPre = styled.pre<{ theme: any }>`
|
||||||
|
& .hljs {
|
||||||
|
color: var(--vscode-editor-foreground, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
${(props) =>
|
||||||
|
Object.keys(props.theme)
|
||||||
|
.map((key, index) => {
|
||||||
|
return `
|
||||||
|
& ${key} {
|
||||||
|
color: ${props.theme[key]};
|
||||||
|
}
|
||||||
|
`
|
||||||
|
})
|
||||||
|
.join("")}
|
||||||
|
`
|
||||||
|
|
||||||
|
const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
|
||||||
|
const { theme } = useExtensionState()
|
||||||
|
const [reactContent, setMarkdown] = useRemark({
|
||||||
|
remarkPlugins: [
|
||||||
|
() => {
|
||||||
|
return (tree) => {
|
||||||
|
visit(tree, "code", (node: any) => {
|
||||||
|
if (!node.lang) {
|
||||||
|
node.lang = "javascript"
|
||||||
|
} else if (node.lang.includes(".")) {
|
||||||
|
node.lang = node.lang.split(".").slice(-1)[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
rehypePlugins: [
|
||||||
|
rehypeHighlight as any,
|
||||||
|
{
|
||||||
|
// languages: {},
|
||||||
|
} as Options,
|
||||||
|
],
|
||||||
|
rehypeReactOptions: {
|
||||||
|
components: {
|
||||||
|
pre: ({ node, ...preProps }: any) => <StyledPre {...preProps} theme={theme} />,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMarkdown(markdown || "")
|
||||||
|
}, [markdown, setMarkdown, theme])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{}}>
|
||||||
|
<StyledMarkdown>{reactContent}</StyledMarkdown>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default MarkdownBlock
|
||||||
Reference in New Issue
Block a user