diff --git a/packages/platforms/vscode/package.json b/packages/platforms/vscode/package.json
index 5e4c283..e2ae7f2 100644
--- a/packages/platforms/vscode/package.json
+++ b/packages/platforms/vscode/package.json
@@ -98,6 +98,8 @@
"diff": "^8.0.0",
"dompurify": "^3.3.1",
"highlight.js": "^11.11.1",
+ "katex": "^0.17.0",
+ "marked-katex-extension": "^5.1.10",
"react-icons": "^5.5.0"
}
}
diff --git a/packages/platforms/vscode/src/chat-view-provider.ts b/packages/platforms/vscode/src/chat-view-provider.ts
index 0dc4f21..718ac8a 100644
--- a/packages/platforms/vscode/src/chat-view-provider.ts
+++ b/packages/platforms/vscode/src/chat-view-provider.ts
@@ -380,7 +380,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider {
+ content="default-src 'none'; style-src ${webview.cspSource} 'nonce-${nonce}'; style-src-attr 'unsafe-inline'; style-src-elem ${webview.cspSource} 'nonce-${nonce}'; script-src 'nonce-${nonce}';" />
diff --git a/packages/platforms/vscode/webview/__tests__/components/molecules/TextPartView.test.tsx b/packages/platforms/vscode/webview/__tests__/components/molecules/TextPartView.test.tsx
index f6e0dd8..d85a0e7 100644
--- a/packages/platforms/vscode/webview/__tests__/components/molecules/TextPartView.test.tsx
+++ b/packages/platforms/vscode/webview/__tests__/components/molecules/TextPartView.test.tsx
@@ -12,7 +12,7 @@ describe("TextPartView", () => {
it("HTML コンテンツをレンダリングすること", () => {
const part = createTextPart("Hello world");
const { container } = render();
- expect(container.querySelector("span")).toBeInTheDocument();
+ expect(container.querySelector(".markdown")).toBeInTheDocument();
});
// renders the text
@@ -150,4 +150,21 @@ describe("TextPartView", () => {
spy.mockRestore();
});
});
+
+ // KaTeX display math with \tag{} renders a .tag element inside .katex-html
+ context("\\tag{} 付きディスプレイ数式の場合", () => {
+ it("\\tag{} 要素がレンダリングされること", () => {
+ const spy = vi
+ .spyOn(Marked.prototype, "parse")
+ .mockReturnValueOnce(
+ 'x+y2x(1)',
+ );
+ const part = createTextPart("$$\\tag{1} x+y^{2x}$$");
+ const { container } = render();
+ expect(container.querySelector(".katex-display")).toBeInTheDocument();
+ expect(container.querySelector(".katex-display .katex .katex-html .tag")).toBeInTheDocument();
+ expect(container.querySelector(".tag")!.textContent).toBe("(1)");
+ spy.mockRestore();
+ });
+ });
});
diff --git a/packages/platforms/vscode/webview/components/molecules/TextPartView/TextPartView.tsx b/packages/platforms/vscode/webview/components/molecules/TextPartView/TextPartView.tsx
index f0fa4f4..5e8cd5b 100644
--- a/packages/platforms/vscode/webview/components/molecules/TextPartView/TextPartView.tsx
+++ b/packages/platforms/vscode/webview/components/molecules/TextPartView/TextPartView.tsx
@@ -2,6 +2,7 @@ import type { TextPart } from "@opencodegui/core";
import DOMPurify from "dompurify";
import hljs from "highlight.js/lib/common";
import { Marked, type Renderer, type Tokens } from "marked";
+import markedKatex from "marked-katex-extension";
import { createElement, useCallback, useMemo } from "react";
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
@@ -154,7 +155,11 @@ function linkifyAbsolutePaths(html: string): string {
}
// marked インスタンス(グローバル状態を汚染しない)
-const markdownParser = new Marked({ breaks: true }, { renderer: { ...codeRenderer, ...linkRenderer } });
+const markdownParser = new Marked(
+ { breaks: true },
+ { renderer: { ...codeRenderer, ...linkRenderer } },
+ markedKatex({ throwOnError: false, nonStandard: true }),
+);
type Props = {
part: TextPart;
@@ -206,5 +211,5 @@ export function TextPartView({ part }: Props) {
// biome-ignore lint/security/noDangerouslySetInnerHtml: DOMPurify でサニタイズ済みの HTML を描画する
// biome-ignore lint/a11y/useKeyWithClickEvents: コピーボタンとファイルリンクのイベント委譲
- return ;
+ return ;
}
diff --git a/packages/platforms/vscode/webview/components/organisms/MessageItem/MessageItem.module.css b/packages/platforms/vscode/webview/components/organisms/MessageItem/MessageItem.module.css
index 4749904..fa16693 100644
--- a/packages/platforms/vscode/webview/components/organisms/MessageItem/MessageItem.module.css
+++ b/packages/platforms/vscode/webview/components/organisms/MessageItem/MessageItem.module.css
@@ -223,6 +223,41 @@
margin-top: 0;
}
+/* --- KaTeX (math) --- */
+
+.content :global(.markdown .katex) {
+ font-size: 1.05em;
+ line-height: 1.25;
+ text-rendering: auto;
+}
+
+.content :global(.markdown .katex-display) {
+ display: block;
+ margin: 10px 0;
+ padding: 4px 0 10px;
+ overflow-x: auto;
+ overflow-y: visible;
+ line-height: 1.25;
+ text-align: center;
+}
+
+.content :global(.markdown .katex-display > .katex) {
+ display: block;
+ max-width: none;
+ white-space: nowrap;
+ text-align: center;
+ vertical-align: baseline;
+}
+
+.content :global(.markdown .katex-display > .katex > .katex-html) {
+ width: max-content;
+ min-width: 100%;
+}
+
+.content :global(.markdown p > .katex) {
+ vertical-align: -0.08em;
+}
+
/* --- Code block wrapper with header & copy button --- */
.content :global(.code-block-wrapper) {
diff --git a/packages/platforms/vscode/webview/main.tsx b/packages/platforms/vscode/webview/main.tsx
index 9891129..49fa970 100644
--- a/packages/platforms/vscode/webview/main.tsx
+++ b/packages/platforms/vscode/webview/main.tsx
@@ -1,6 +1,7 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
+import "katex/dist/katex.min.css";
import "./styles.css";
createRoot(document.getElementById("root")!).render(
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 1b59ff2..fba353b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -51,6 +51,12 @@ importers:
highlight.js:
specifier: ^11.11.1
version: 11.11.1
+ katex:
+ specifier: ^0.17.0
+ version: 0.17.0
+ marked-katex-extension:
+ specifier: ^5.1.10
+ version: 5.1.10(katex@0.17.0)(marked@17.0.4)
react-icons:
specifier: ^5.5.0
version: 5.6.0(react@19.2.4)
@@ -287,28 +293,24 @@ packages:
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@biomejs/cli-linux-arm64@2.4.4':
resolution: {integrity: sha512-V/NFfbWhsUU6w+m5WYbBenlEAz8eYnSqRMDMAW3K+3v0tYVkNyZn8VU0XPxk/lOqNXLSCCrV7FmV/u3SjCBShg==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@biomejs/cli-linux-x64-musl@2.4.4':
resolution: {integrity: sha512-gGvFTGpOIQDb5CQ2VC0n9Z2UEqlP46c4aNgHmAMytYieTGEcfqhfCFnhs6xjt0S3igE6q5GLuIXtdQt3Izok+g==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
- libc: [musl]
'@biomejs/cli-linux-x64@2.4.4':
resolution: {integrity: sha512-R4+ZCDtG9kHArasyBO+UBD6jr/FcFCTH8QkNTOCu0pRJzCWyWC4EtZa2AmUZB5h3e0jD7bRV2KvrENcf8rndBg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@biomejs/cli-win32-arm64@2.4.4':
resolution: {integrity: sha512-trzCqM7x+Gn832zZHgr28JoYagQNX4CZkUZhMUac2YxvvyDRLJDrb5m9IA7CaZLlX6lTQmADVfLEKP1et1Ma4Q==}
@@ -594,79 +596,66 @@ packages:
resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==}
cpu: [arm]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-arm-musleabihf@4.59.0':
resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==}
cpu: [arm]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-arm64-gnu@4.59.0':
resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==}
cpu: [arm64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-arm64-musl@4.59.0':
resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==}
cpu: [arm64]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-loong64-gnu@4.59.0':
resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==}
cpu: [loong64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-loong64-musl@4.59.0':
resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==}
cpu: [loong64]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-ppc64-gnu@4.59.0':
resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==}
cpu: [ppc64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-ppc64-musl@4.59.0':
resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==}
cpu: [ppc64]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-riscv64-gnu@4.59.0':
resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==}
cpu: [riscv64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-riscv64-musl@4.59.0':
resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==}
cpu: [riscv64]
os: [linux]
- libc: [musl]
'@rollup/rollup-linux-s390x-gnu@4.59.0':
resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==}
cpu: [s390x]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-x64-gnu@4.59.0':
resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==}
cpu: [x64]
os: [linux]
- libc: [glibc]
'@rollup/rollup-linux-x64-musl@4.59.0':
resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==}
cpu: [x64]
os: [linux]
- libc: [musl]
'@rollup/rollup-openbsd-x64@4.59.0':
resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==}
@@ -1100,6 +1089,10 @@ packages:
resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==}
engines: {node: '>=18'}
+ commander@8.3.0:
+ resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
+ engines: {node: '>= 12'}
+
concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@@ -1528,6 +1521,10 @@ packages:
jws@4.0.1:
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
+ katex@0.17.0:
+ resolution: {integrity: sha512-Vdw0ATsQ9V+LuegM/BTwQqV/6cTl5lbGcIrU+BCgLxyf6bo38ybOr372tuSIxir3CN720flu1meYR6XzNMwQnw==}
+ hasBin: true
+
keytar@7.9.0:
resolution: {integrity: sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==}
@@ -1590,6 +1587,12 @@ packages:
resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==}
hasBin: true
+ marked-katex-extension@5.1.10:
+ resolution: {integrity: sha512-TuqrzguLeXXm6iBaf16leL3+dVmMj8KrBdunMVVzxMS/bwcjtQ0YG0sNytl1j7uUo8yClsXJqBbVjH1yOPurwQ==}
+ peerDependencies:
+ katex: '>=0.16 <0.18'
+ marked: '>=4 <19'
+
marked@17.0.4:
resolution: {integrity: sha512-NOmVMM+KAokHMvjWmC5N/ZOvgmSWuqJB8FoYI019j4ogb/PeRMKoKIjReZ2w3376kkA8dSJIP8uD993Kxc0iRQ==}
engines: {node: '>= 20'}
@@ -2118,6 +2121,7 @@ packages:
uuid@8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
+ deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).
hasBin: true
validate-npm-package-license@3.0.4:
@@ -3248,6 +3252,8 @@ snapshots:
commander@12.1.0: {}
+ commander@8.3.0: {}
+
concat-map@0.0.1: {}
convert-source-map@2.0.0: {}
@@ -3719,6 +3725,10 @@ snapshots:
jwa: 2.0.1
safe-buffer: 5.2.1
+ katex@0.17.0:
+ dependencies:
+ commander: 8.3.0
+
keytar@7.9.0:
dependencies:
node-addon-api: 4.3.0
@@ -3776,6 +3786,11 @@ snapshots:
punycode.js: 2.3.1
uc.micro: 2.1.0
+ marked-katex-extension@5.1.10(katex@0.17.0)(marked@17.0.4):
+ dependencies:
+ katex: 0.17.0
+ marked: 17.0.4
+
marked@17.0.4: {}
math-intrinsics@1.1.0: {}