Răsfoiți Sursa

Merge pull request #1296 from RooVetGit/mermaids

Mermaids
Matt Rubens 10 luni în urmă
părinte
comite
22c8a683d8

+ 11 - 3
src/core/prompts/__tests__/__snapshots__/system.test.ts.snap

@@ -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:

+ 2 - 2
src/shared/modes.ts

@@ -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",

Fișier diff suprimat deoarece este prea mare
+ 868 - 2
webview-ui/package-lock.json


+ 1 - 0
webview-ui/package.json

@@ -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",

+ 23 - 2
webview-ui/src/components/common/MarkdownBlock.tsx

@@ -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} />
+				},
 			},
 		},
 	})

+ 227 - 0
webview-ui/src/components/common/MermaidBlock.tsx

@@ -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;
+`

+ 42 - 0
webview-ui/src/utils/useDebounceEffect.ts

@@ -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 re‐schedule if any item in `deps` changed,
+		// or if `delay` changed.
+
+		// eslint-disable-next-line react-hooks/exhaustive-deps
+	}, [delay, ...deps])
+}

Unele fișiere nu au fost afișate deoarece prea multe fișiere au fost modificate în acest diff