Просмотр исходного кода

feat: LaTeX formatting (#3242)

* initial

* initial

* restored package-lock.json

* restored comment

* One line

* prettier

* do not throw on error

* escape backslashes in system notification

* better prompt

* reduce prompt size

* prettier

---------

Co-authored-by: canvrno <[email protected]>
Frostbourne 8 месяцев назад
Родитель
Сommit
eb6e4818d3

+ 5 - 0
.changeset/curly-guests-raise.md

@@ -0,0 +1,5 @@
+---
+"claude-dev": minor
+---
+
+Full support for LaTeX rendering

+ 5 - 1
.vscodeignore

@@ -37,8 +37,12 @@ docs/**
 !node_modules/@vscode/codicons/dist/codicon.css
 !node_modules/@vscode/codicons/dist/codicon.ttf
 
+# Include KaTeX CSS and fonts for LaTeX rendering
+!webview-ui/node_modules/katex/dist/katex.min.css
+!webview-ui/node_modules/katex/dist/fonts/**
+
 # Include default themes JSON files used in getTheme
 !src/integrations/theme/default-themes/**
 
 # Include icons
-!assets/icons/**
+!assets/icons/**

+ 1 - 0
src/core/prompts/system.ts

@@ -573,6 +573,7 @@ CAPABILITIES
 		: ""
 }
 - You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
+- You can use LaTeX syntax in your responses to render mathematical expressions
 
 ====
 

+ 38 - 19
src/core/webview/index.ts

@@ -181,6 +181,14 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
 			"codicon.css",
 		])
 
+		const katexCssUri = getUri(webview, this.context.extensionUri, [
+			"webview-ui",
+			"node_modules",
+			"katex",
+			"dist",
+			"katex.min.css",
+		])
+
 		// const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.js"))
 
 		// const styleResetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "reset.css"))
@@ -204,24 +212,25 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
 
 		// Tip: Install the es6-string-html VS Code extension to enable code highlighting below
 		return /*html*/ `
-        <!DOCTYPE html>
-        <html lang="en">
-          <head>
-            <meta charset="utf-8">
-            <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
-            <meta name="theme-color" content="#000000">
-            <link rel="stylesheet" type="text/css" href="${stylesUri}">
-            <link href="${codiconsUri}" rel="stylesheet" />
-						<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src https://*.posthog.com https://*.firebaseauth.com https://*.firebaseio.com https://*.googleapis.com https://*.firebase.com; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https: data:; script-src 'nonce-${nonce}' 'unsafe-eval';">
-            <title>Cline</title>
-          </head>
-          <body>
-            <noscript>You need to enable JavaScript to run this app.</noscript>
-            <div id="root"></div>
-            <script type="module" nonce="${nonce}" src="${scriptUri}"></script>
-          </body>
-        </html>
-      `
+			<!DOCTYPE html>
+			<html lang="en">
+				<head>
+				<meta charset="utf-8">
+				<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
+				<meta name="theme-color" content="#000000">
+				<link rel="stylesheet" type="text/css" href="${stylesUri}">
+				<link href="${codiconsUri}" rel="stylesheet" />
+				<link href="${katexCssUri}" rel="stylesheet" />
+				<meta http-equiv="Content-Security-Policy" content="default-src 'none'; connect-src https://*.posthog.com https://*.firebaseauth.com https://*.firebaseio.com https://*.googleapis.com https://*.firebase.com; font-src ${webview.cspSource} data:; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https: data:; script-src 'nonce-${nonce}' 'unsafe-eval';">
+				<title>Cline</title>
+			</head>
+			<body>
+				<noscript>You need to enable JavaScript to run this app.</noscript>
+				<div id="root"></div>
+				<script type="module" nonce="${nonce}" src="${scriptUri}"></script>
+			</body>
+		</html>
+		`
 	}
 
 	/**
@@ -256,6 +265,15 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
 			"codicon.css",
 		])
 
+		// Get KaTeX resources
+		const katexCssUri = getUri(webview, this.context.extensionUri, [
+			"webview-ui",
+			"node_modules",
+			"katex",
+			"dist",
+			"katex.min.css",
+		])
+
 		const scriptEntrypoint = "src/main.tsx"
 		const scriptUri = `http://${localServerUrl}/${scriptEntrypoint}`
 
@@ -271,7 +289,7 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
 
 		const csp = [
 			"default-src 'none'",
-			`font-src ${webview.cspSource}`,
+			`font-src ${webview.cspSource} data:`,
 			`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
 			`img-src ${webview.cspSource} https: data:`,
 			`script-src 'unsafe-eval' https://* http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
@@ -287,6 +305,7 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
 					<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
 					<link rel="stylesheet" type="text/css" href="${stylesUri}">
 					<link href="${codiconsUri}" rel="stylesheet" />
+					<link href="${katexCssUri}" rel="stylesheet" />
 					<title>Cline</title>
 				</head>
 				<body>

+ 1 - 1
src/integrations/notifications/index.ts

@@ -74,7 +74,7 @@ export async function showSystemNotification(options: NotificationOptions): Prom
 		const escapedOptions = {
 			...options,
 			title: title.replace(/"/g, '\\"'),
-			message: message.replace(/"/g, '\\"'),
+			message: message.replace(/\\/g, "\\\\").replace(/"/g, '\\"'),
 			subtitle: options.subtitle?.replace(/"/g, '\\"') || "",
 		}
 

Разница между файлами не показана из-за своего большого размера
+ 329 - 1183
webview-ui/package-lock.json


+ 4 - 0
webview-ui/package.json

@@ -24,6 +24,7 @@
 		"framer-motion": "^12.7.4",
 		"fuse.js": "^7.0.0",
 		"fzf": "^0.5.2",
+		"katex": "^0.16.22",
 		"mermaid": "^11.4.1",
 		"posthog-js": "^1.224.0",
 		"pretty-bytes": "^6.1.1",
@@ -35,8 +36,10 @@
 		"react-use": "^17.6.0",
 		"react-virtuoso": "^4.12.3",
 		"rehype-highlight": "^7.0.1",
+		"rehype-katex": "^7.0.1",
 		"rehype-parse": "^9.0.1",
 		"rehype-remark": "^10.0.1",
+		"remark-math": "^6.0.0",
 		"remark-stringify": "^11.0.0",
 		"styled-components": "^6.1.15",
 		"unified": "^11.0.5",
@@ -50,6 +53,7 @@
 		"@testing-library/user-event": "^14.6.1",
 		"@types/dompurify": "^3.0.5",
 		"@types/jest": "^29.5.14",
+		"@types/katex": "^0.16.7",
 		"@types/node": "^22.13.4",
 		"@types/react": "^18.3.18",
 		"@types/react-dom": "^18.3.5",

+ 32 - 0
webview-ui/src/components/common/MarkdownBlock.tsx

@@ -2,6 +2,8 @@ import React, { memo, useEffect, useRef, useState } from "react"
 import type { ComponentProps } from "react"
 import { useRemark } from "react-remark"
 import rehypeHighlight, { Options } from "rehype-highlight"
+import rehypeKatex from "rehype-katex"
+import remarkMath from "remark-math"
 import styled from "styled-components"
 import { visit } from "unist-util-visit"
 import type { Node } from "unist"
@@ -157,6 +159,34 @@ const StyledMarkdown = styled.div`
 		overflow-wrap: anywhere;
 	}
 
+	/* KaTeX styling */
+	.katex {
+		font-size: 1.1em;
+		color: var(--vscode-editor-foreground);
+		font-family: KaTeX_Main, "Times New Roman", serif;
+		line-height: 1.2;
+		white-space: normal;
+		text-indent: 0;
+	}
+
+	.katex-display {
+		display: block;
+		margin: 1em 0;
+		text-align: center;
+		padding: 0.5em;
+		overflow-x: auto;
+		overflow-y: hidden;
+		background-color: var(--vscode-textCodeBlock-background);
+		border-radius: 3px;
+	}
+
+	.katex-error {
+		color: var(--vscode-errorForeground);
+		border: 1px solid var(--vscode-inputValidation-errorBorder);
+		padding: 8px;
+		border-radius: 3px;
+	}
+
 	font-family:
 		var(--vscode-font-family),
 		system-ui,
@@ -256,6 +286,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
 		remarkPlugins: [
 			remarkPreventBoldFilenames,
 			remarkUrlToLink,
+			remarkMath,
 			() => {
 				return (tree) => {
 					visit(tree, "code", (node: any) => {
@@ -273,6 +304,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => {
 			{
 				// languages: {},
 			} as Options,
+			rehypeKatex,
 		],
 		rehypeReactOptions: {
 			components: {

+ 1 - 0
webview-ui/src/index.css

@@ -3,6 +3,7 @@
 /* Disable Tailwind's CSS reset to preserve existing styles */
 /* @import "tailwindcss/preflight.css" layer(base); */
 @import "tailwindcss/utilities.css" layer(utilities);
+@import "katex/dist/katex.min.css";
 
 @config "../tailwind.config.js";
 

+ 9 - 1
webview-ui/vite.config.ts

@@ -23,7 +23,15 @@ export default defineConfig({
 				inlineDynamicImports: true,
 				entryFileNames: `assets/[name].js`,
 				chunkFileNames: `assets/[name].js`,
-				assetFileNames: `assets/[name].[ext]`,
+				assetFileNames: (assetInfo) => {
+					if (
+						assetInfo.name &&
+						(assetInfo.name.endsWith(".woff2") || assetInfo.name.endsWith(".woff") || assetInfo.name.endsWith(".ttf"))
+					) {
+						return "assets/fonts/[name][extname]"
+					}
+					return "assets/[name][extname]"
+				},
 			},
 		},
 		chunkSizeWarningLimit: 100000,

Некоторые файлы не были показаны из-за большого количества измененных файлов