Merge pull request #1296 from RooVetGit/mermaids

Mermaids
This commit is contained in:
Matt Rubens 2025-03-01 18:51:40 -05:00 committed by GitHub
commit 22c8a683d8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1428 additions and 12 deletions

View file

@ -3899,9 +3899,17 @@ USER'S CUSTOM INSTRUCTIONS
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
Mode-specific Instructions:
Depending on the user's request, you may need to do some information gathering (for example using read_file or search_files) to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. (You can write the plan to a markdown file if it seems appropriate.)
1. Do some information gathering (for example using read_file or search_files) to get more context about the task.
Then you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it. Finally once it seems like you've reached a good plan, use the switch_mode tool to request that the user switch to another mode to implement the solution.
2. You should also ask the user clarifying questions to get a better understanding of the task.
3. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. Include Mermaid diagrams if they help make your plan clearer.
4. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.
5. Once the user confirms the plan, ask them if they'd like you to write it to a markdown file.
6. Use the switch_mode tool to request that the user switch to another mode to implement the solution.
Rules:
# Rules from .clinerules-architect:
@ -4176,7 +4184,7 @@ USER'S CUSTOM INSTRUCTIONS
The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
Mode-specific Instructions:
You can analyze code, explain concepts, and access external resources. Make sure to answer the user's questions and don't rush to switch to implementing code.
You can analyze code, explain concepts, and access external resources. Make sure to answer the user's questions and don't rush to switch to implementing code. Include Mermaid diagrams if they help make your response clearer.
Rules:
# Rules from .clinerules-ask:

View file

@ -92,7 +92,7 @@ export const modes: readonly ModeConfig[] = [
"You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution.",
groups: ["read", ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }], "browser", "mcp"],
customInstructions:
"Depending on the user's request, you may need to do some information gathering (for example using read_file or search_files) to get more context about the task. You may also ask the user clarifying questions to get a better understanding of the task. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. (You can write the plan to a markdown file if it seems appropriate.)\n\nThen you might ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it. Finally once it seems like you've reached a good plan, use the switch_mode tool to request that the user switch to another mode to implement the solution.",
"1. Do some information gathering (for example using read_file or search_files) to get more context about the task.\n\n2. You should also ask the user clarifying questions to get a better understanding of the task.\n\n3. Once you've gained more context about the user's request, you should create a detailed plan for how to accomplish the task. Include Mermaid diagrams if they help make your plan clearer.\n\n4. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and plan the best way to accomplish it.\n\n5. Once the user confirms the plan, ask them if they'd like you to write it to a markdown file.\n\n6. Use the switch_mode tool to request that the user switch to another mode to implement the solution.",
},
{
slug: "ask",
@ -101,7 +101,7 @@ export const modes: readonly ModeConfig[] = [
"You are Roo, a knowledgeable technical assistant focused on answering questions and providing information about software development, technology, and related topics.",
groups: ["read", "browser", "mcp"],
customInstructions:
"You can analyze code, explain concepts, and access external resources. Make sure to answer the user's questions and don't rush to switch to implementing code.",
"You can analyze code, explain concepts, and access external resources. Make sure to answer the user's questions and don't rush to switch to implementing code. Include Mermaid diagrams if they help make your response clearer.",
},
{
slug: "debug",

File diff suppressed because it is too large Load diff

View file

@ -35,6 +35,7 @@
"fast-deep-equal": "^3.1.3",
"fzf": "^0.5.2",
"lucide-react": "^0.475.0",
"mermaid": "^11.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.3",

View file

@ -1,10 +1,11 @@
import { memo, useEffect } from "react"
import React, { 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"
import MermaidBlock from "./MermaidBlock"
interface MarkdownBlockProps {
markdown?: string
@ -182,7 +183,27 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
],
rehypeReactOptions: {
components: {
pre: ({ node, ...preProps }: any) => <StyledPre {...preProps} theme={theme} />,
pre: ({ node, children, ...preProps }: any) => {
if (Array.isArray(children) && children.length === 1 && React.isValidElement(children[0])) {
const child = children[0] as React.ReactElement<{ className?: string }>
if (child.props?.className?.includes("language-mermaid")) {
return child
}
}
return (
<StyledPre {...preProps} theme={theme}>
{children}
</StyledPre>
)
},
code: (props: any) => {
const className = props.className || ""
if (className.includes("language-mermaid")) {
const codeText = String(props.children || "")
return <MermaidBlock code={codeText} />
}
return <code {...props} />
},
},
},
})

View file

@ -0,0 +1,227 @@
import { useEffect, useRef, useState } from "react"
import mermaid from "mermaid"
import { useDebounceEffect } from "../../utils/useDebounceEffect"
import styled from "styled-components"
import { vscode } from "../../utils/vscode"
const MERMAID_THEME = {
background: "#1e1e1e", // VS Code dark theme background
textColor: "#ffffff", // Main text color
mainBkg: "#2d2d2d", // Background for nodes
nodeBorder: "#888888", // Border color for nodes
lineColor: "#cccccc", // Lines connecting nodes
primaryColor: "#3c3c3c", // Primary color for highlights
primaryTextColor: "#ffffff", // Text in primary colored elements
primaryBorderColor: "#888888",
secondaryColor: "#2d2d2d", // Secondary color for alternate elements
tertiaryColor: "#454545", // Third color for special elements
// Class diagram specific
classText: "#ffffff",
// State diagram specific
labelColor: "#ffffff",
// Sequence diagram specific
actorLineColor: "#cccccc",
actorBkg: "#2d2d2d",
actorBorder: "#888888",
actorTextColor: "#ffffff",
// Flow diagram specific
fillType0: "#2d2d2d",
fillType1: "#3c3c3c",
fillType2: "#454545",
}
mermaid.initialize({
startOnLoad: false,
securityLevel: "loose",
theme: "dark",
themeVariables: {
...MERMAID_THEME,
fontSize: "16px",
fontFamily: "var(--vscode-font-family, 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif)",
// Additional styling
noteTextColor: "#ffffff",
noteBkgColor: "#454545",
noteBorderColor: "#888888",
// Improve contrast for special elements
critBorderColor: "#ff9580",
critBkgColor: "#803d36",
// Task diagram specific
taskTextColor: "#ffffff",
taskTextOutsideColor: "#ffffff",
taskTextLightColor: "#ffffff",
// Numbers/sections
sectionBkgColor: "#2d2d2d",
sectionBkgColor2: "#3c3c3c",
// Alt sections in sequence diagrams
altBackground: "#2d2d2d",
// Links
linkColor: "#6cb6ff",
// Borders and lines
compositeBackground: "#2d2d2d",
compositeBorder: "#888888",
titleColor: "#ffffff",
},
})
interface MermaidBlockProps {
code: string
}
export default function MermaidBlock({ code }: MermaidBlockProps) {
const containerRef = useRef<HTMLDivElement>(null)
const [isLoading, setIsLoading] = useState(false)
// 1) Whenever `code` changes, mark that we need to re-render a new chart
useEffect(() => {
setIsLoading(true)
}, [code])
// 2) Debounce the actual parse/render
useDebounceEffect(
() => {
if (containerRef.current) {
containerRef.current.innerHTML = ""
}
mermaid
.parse(code, { suppressErrors: true })
.then((isValid) => {
if (!isValid) {
throw new Error("Invalid or incomplete Mermaid code")
}
const id = `mermaid-${Math.random().toString(36).substring(2)}`
return mermaid.render(id, code)
})
.then(({ svg }) => {
if (containerRef.current) {
containerRef.current.innerHTML = svg
}
})
.catch((err) => {
console.warn("Mermaid parse/render failed:", err)
containerRef.current!.innerHTML = code.replace(/</g, "&lt;").replace(/>/g, "&gt;")
})
.finally(() => {
setIsLoading(false)
})
},
500, // Delay 500ms
[code], // Dependencies for scheduling
)
/**
* Called when user clicks the rendered diagram.
* Converts the <svg> to a PNG and sends it to the extension.
*/
const handleClick = async () => {
if (!containerRef.current) return
const svgEl = containerRef.current.querySelector("svg")
if (!svgEl) return
try {
const pngDataUrl = await svgToPng(svgEl)
vscode.postMessage({
type: "openImage",
text: pngDataUrl,
})
} catch (err) {
console.error("Error converting SVG to PNG:", err)
}
}
return (
<MermaidBlockContainer>
{isLoading && <LoadingMessage>Generating mermaid diagram...</LoadingMessage>}
{/* The container for the final <svg> or raw code. */}
<SvgContainer onClick={handleClick} ref={containerRef} $isLoading={isLoading} />
</MermaidBlockContainer>
)
}
async function svgToPng(svgEl: SVGElement): Promise<string> {
console.log("svgToPng function called")
// Clone the SVG to avoid modifying the original
const svgClone = svgEl.cloneNode(true) as SVGElement
// Get the original viewBox
const viewBox = svgClone.getAttribute("viewBox")?.split(" ").map(Number) || []
const originalWidth = viewBox[2] || svgClone.clientWidth
const originalHeight = viewBox[3] || svgClone.clientHeight
// Calculate the scale factor to fit editor width while maintaining aspect ratio
// Unless we can find a way to get the actual editor window dimensions through the VS Code API (which might be possible but would require changes to the extension side),
// the fixed width seems like a reliable approach.
const editorWidth = 3_600
const scale = editorWidth / originalWidth
const scaledHeight = originalHeight * scale
// Update SVG dimensions
svgClone.setAttribute("width", `${editorWidth}`)
svgClone.setAttribute("height", `${scaledHeight}`)
const serializer = new XMLSerializer()
const svgString = serializer.serializeToString(svgClone)
const svgDataUrl = "data:image/svg+xml;base64," + btoa(decodeURIComponent(encodeURIComponent(svgString)))
return new Promise((resolve, reject) => {
const img = new Image()
img.onload = () => {
const canvas = document.createElement("canvas")
canvas.width = editorWidth
canvas.height = scaledHeight
const ctx = canvas.getContext("2d")
if (!ctx) return reject("Canvas context not available")
// Fill background with Mermaid's dark theme background color
ctx.fillStyle = MERMAID_THEME.background
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.imageSmoothingEnabled = true
ctx.imageSmoothingQuality = "high"
ctx.drawImage(img, 0, 0, editorWidth, scaledHeight)
resolve(canvas.toDataURL("image/png", 1.0))
}
img.onerror = reject
img.src = svgDataUrl
})
}
const MermaidBlockContainer = styled.div`
position: relative;
margin: 8px 0;
`
const LoadingMessage = styled.div`
padding: 8px 0;
color: var(--vscode-descriptionForeground);
font-style: italic;
font-size: 0.9em;
`
interface SvgContainerProps {
$isLoading: boolean
}
const SvgContainer = styled.div<SvgContainerProps>`
opacity: ${(props) => (props.$isLoading ? 0.3 : 1)};
min-height: 20px;
transition: opacity 0.2s ease;
cursor: pointer;
display: flex;
justify-content: center;
`

View file

@ -0,0 +1,42 @@
import { useEffect, useRef } from "react"
type VoidFn = () => void
/**
* Runs `effectRef.current()` after `delay` ms whenever any of the `deps` change,
* but cancels/re-schedules if they change again before the delay.
*/
export function useDebounceEffect(effect: VoidFn, delay: number, deps: any[]) {
const callbackRef = useRef<VoidFn>(effect)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
// Keep callbackRef current
useEffect(() => {
callbackRef.current = effect
}, [effect])
useEffect(() => {
// Clear any queued call
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
// Schedule a new call
timeoutRef.current = setTimeout(() => {
// always call the *latest* version of effect
callbackRef.current()
}, delay)
// Cleanup on unmount or next effect
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current)
}
}
// We want to reschedule if any item in `deps` changed,
// or if `delay` changed.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [delay, ...deps])
}