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 <kevin@cline.bot>
This commit is contained in:
Frostbourne 2025-05-02 07:16:59 -07:00 committed by GitHub
parent 61d2f42955
commit eb6e4818d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 612 additions and 1035 deletions

View File

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

View File

@ -37,6 +37,10 @@ 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/**

View File

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

View File

@ -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"))
@ -212,7 +220,8 @@ export class WebviewProvider implements vscode.WebviewViewProvider {
<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';">
<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>
@ -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>

View File

@ -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, '\\"') || "",
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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