diff --git a/docs/acceptance/chat-return-latest-20261022/README.md b/docs/acceptance/chat-return-latest-20261022/README.md new file mode 100644 index 00000000..50cf24eb --- /dev/null +++ b/docs/acceptance/chat-return-latest-20261022/README.md @@ -0,0 +1,33 @@ +# 长对话返回最新位置验收 + +任务结束后向上翻看长回复,原页面没有回到底部的入口,只有新内容到达后才显示按钮。现在离开底部时即可使用「回到最新」,有新内容时继续使用原来的提示。搜索定位仍由原来的「返回最新消息」退出历史范围。 + +## 真实页面 + +- 前:`a07575e9ee863f94cf65d5add5623d27b7358d69`,PR #454 的前端基线。 +- 后:本 PR 的 `conversation.tsx` 更新。 +- Windows / Microsoft Edge,1280×900 与 480×600;均为实际生产前端包。 +- 同一生产 Go API、隔离工作区、同一持久化对话。通过正常项目导入、创建对话和发送消息,由离线测试模型返回已完成的 48 段回复。没有直接改数据库、接口响应或 DOM。 +- 长对话:`thread-run-20261021202227-f4bbfb342eaa`;回复 `evt-20261021202227-73a5553bb0de`。 +- 短对话:`thread-run-20261021202227-0d3fe6eef297`。 +- API 确认两条回复均已保存,`durable=true`、`provisional=false`,且对应 Agent 回合完成。 + +基线向上滚动后没有返回按钮。候选页面向上滚动后显示按钮,且未标成有新内容。切换到短对话时按钮消失;切回长对话,阅读位置仍为 1395px,草稿仍在。鼠标点击或窄窗口下聚焦后按 Enter,均回到真实底部(`scrollHeight - clientHeight - scrollTop = 0`),按钮随之隐藏。 + +截图前将验收草稿清空并关闭无关的浏览器提示,便于比较同一回复位置。 + +| 场景 | 修改前 | 修改后 | +| --- | --- | --- | +| 桌面翻看已完成回复 | ![修改前](before.png) | ![修改后](after.png) | +| 480×600 | ![窄窗口修改前](before-narrow.png) | ![窄窗口修改后](after-narrow.png) | + +实际点击后回到末尾: + +![已回到回复末尾](after-latest.png) + +## 检查 + +- 新回归测试在原代码上因找不到「回到最新」失败,修改后通过。 +- `npx vitest run src/v2/components/conversation.test.tsx src/v2/use-reply-reading.test.tsx`:67 项通过,包含流式更新不抢走阅读位置、旧记录分页、读取状态检查。 +- `npm run build`(含 TypeScript 检查)、`git diff --check` 通过。 +- OCR delegate 选出的 1 个产品文件已全部审阅,同时核对测试及 `useV2ReplyReading` 调用,未发现需要修复的问题。 diff --git a/docs/acceptance/chat-return-latest-20261022/after-latest.png b/docs/acceptance/chat-return-latest-20261022/after-latest.png new file mode 100644 index 00000000..fa7ed100 Binary files /dev/null and b/docs/acceptance/chat-return-latest-20261022/after-latest.png differ diff --git a/docs/acceptance/chat-return-latest-20261022/after-narrow.png b/docs/acceptance/chat-return-latest-20261022/after-narrow.png new file mode 100644 index 00000000..dd76ba08 Binary files /dev/null and b/docs/acceptance/chat-return-latest-20261022/after-narrow.png differ diff --git a/docs/acceptance/chat-return-latest-20261022/after.png b/docs/acceptance/chat-return-latest-20261022/after.png new file mode 100644 index 00000000..0f986417 Binary files /dev/null and b/docs/acceptance/chat-return-latest-20261022/after.png differ diff --git a/docs/acceptance/chat-return-latest-20261022/before-narrow.png b/docs/acceptance/chat-return-latest-20261022/before-narrow.png new file mode 100644 index 00000000..224ce85f Binary files /dev/null and b/docs/acceptance/chat-return-latest-20261022/before-narrow.png differ diff --git a/docs/acceptance/chat-return-latest-20261022/before.png b/docs/acceptance/chat-return-latest-20261022/before.png new file mode 100644 index 00000000..7b41f2bf Binary files /dev/null and b/docs/acceptance/chat-return-latest-20261022/before.png differ diff --git a/web/package-lock.json b/web/package-lock.json index 9d8c1bd7..03e7da3c 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -20,6 +20,7 @@ "@tiptap/starter-kit": "3.32.0", "@xterm/addon-fit": "0.11.0", "@xterm/xterm": "6.0.0", + "dompurify": "3.4.17", "highlight.js": "11.11.1", "lucide-react": "1.31.0", "monaco-editor": "0.56.0", @@ -42,7 +43,7 @@ "openapi-typescript": "7.13.0", "typescript": "5.9.3", "vite": "8.3.4", - "vitest": "4.1.11" + "vitest": "4.1.12" } }, "node_modules/@adobe/css-tools": { @@ -1388,16 +1389,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", - "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.12.tgz", + "integrity": "sha512-vFlTt+YEu9WBpNjdGayNmlMVzrWPi1+sv853/7t2FU7hpO5gAFBKHzQmkCq5Xjpr+evegF2994LQT3hF3mRlqQ==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", + "@vitest/spy": "4.1.12", + "@vitest/utils": "4.1.12", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -1406,13 +1407,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", - "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.12.tgz", + "integrity": "sha512-fKShNpKZzupWZuY0z7egydvL0sqW4INEZBEExMBMAa6izlENEVHwoXgdaTCaBZ5fwlr4xf2o1wXC83n9RpO+NA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.11", + "@vitest/spy": "4.1.12", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -1433,9 +1434,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", - "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.12.tgz", + "integrity": "sha512-XW9O8+Mxx4VpCtkYdMJLZQQwsVryP9wk7cPKDxtXFfwSak6yRh5+2fep/Y0XF/ElBvJHq8bcYdi6aeuai0opZA==", "dev": true, "license": "MIT", "dependencies": { @@ -1446,13 +1447,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", - "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.12.tgz", + "integrity": "sha512-NKlVKCr5QnCe8jijdMnmkpHHU+WTqiLFR6G0pHji9uRjXK9xJl1k1nTw6F3pOOj2PDcsDt2GKQMI7wIsaT491w==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.11", + "@vitest/utils": "4.1.12", "pathe": "^2.0.3" }, "funding": { @@ -1460,14 +1461,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", - "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.12.tgz", + "integrity": "sha512-ko1y1Qgb/slzQ9mr+b89Cuqud8WG1TKtsPD8r7ZUBdvVOGt85Af2HwCqHujrhwJq95hs/9RM7fcAPg57o1vlzw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.11", - "@vitest/utils": "4.1.11", + "@vitest/pretty-format": "4.1.12", + "@vitest/utils": "4.1.12", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1476,9 +1477,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", - "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.12.tgz", + "integrity": "sha512-RGq9x4JNGNTZHEJxvN0oSiJjeSCExm0n2gZKKJZHY9cX5AVA6M7VJILBT3ZJ/tJ/0J9uWT7fETV9/btoi//vSg==", "dev": true, "license": "MIT", "funding": { @@ -1486,13 +1487,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", - "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.12.tgz", + "integrity": "sha512-Q3awXrwOfEV+qc3WmO5ONyE0yO7Q1gQ9/oyHxGLZ/pl7N3NO/ZztqQwKLuwMkV+CA1JYSIeO3gkHnRDIo4AVaA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.11", + "@vitest/pretty-format": "4.1.12", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -1840,9 +1841,9 @@ "peer": true }, "node_modules/dompurify": { - "version": "3.4.16", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", - "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -4281,9 +4282,9 @@ } }, "node_modules/undici": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", - "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", + "version": "7.30.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.1.tgz", + "integrity": "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==", "dev": true, "license": "MIT", "engines": { @@ -4498,19 +4499,19 @@ } }, "node_modules/vitest": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", - "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.12.tgz", + "integrity": "sha512-5eWdz2yb3ajxWF3C8C4TjkXl7FGm3n2E/VlazALrQHzK7nYMSGFnCZto0bbPNl77AwkOZVLk0YnBAejUu0NiWA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.11", - "@vitest/mocker": "4.1.11", - "@vitest/pretty-format": "4.1.11", - "@vitest/runner": "4.1.11", - "@vitest/snapshot": "4.1.11", - "@vitest/spy": "4.1.11", - "@vitest/utils": "4.1.11", + "@vitest/expect": "4.1.12", + "@vitest/mocker": "4.1.12", + "@vitest/pretty-format": "4.1.12", + "@vitest/runner": "4.1.12", + "@vitest/snapshot": "4.1.12", + "@vitest/spy": "4.1.12", + "@vitest/utils": "4.1.12", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -4538,12 +4539,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.11", - "@vitest/browser-preview": "4.1.11", - "@vitest/browser-webdriverio": "4.1.11", - "@vitest/coverage-istanbul": "4.1.11", - "@vitest/coverage-v8": "4.1.11", - "@vitest/ui": "4.1.11", + "@vitest/browser-playwright": "4.1.12", + "@vitest/browser-preview": "4.1.12", + "@vitest/browser-webdriverio": "^4.1.11", + "@vitest/coverage-istanbul": "4.1.12", + "@vitest/coverage-v8": "4.1.12", + "@vitest/ui": "4.1.12", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/web/package.json b/web/package.json index 89d9de27..2f3eed72 100644 --- a/web/package.json +++ b/web/package.json @@ -27,6 +27,7 @@ "@tiptap/starter-kit": "3.32.0", "@xterm/addon-fit": "0.11.0", "@xterm/xterm": "6.0.0", + "dompurify": "3.4.17", "highlight.js": "11.11.1", "lucide-react": "1.31.0", "monaco-editor": "0.56.0", @@ -49,16 +50,16 @@ "openapi-typescript": "7.13.0", "typescript": "5.9.3", "vite": "8.3.4", - "vitest": "4.1.11" + "vitest": "4.1.12" }, "overrides": { "@redocly/openapi-core": { "js-yaml": "4.3.2" }, "brace-expansion": "5.0.12", - "dompurify": "3.4.16", + "dompurify": "3.4.17", "nanoid": "3.3.18", "postcss": "8.5.25", - "undici": "7.29.1" + "undici": "7.30.1" } } diff --git a/web/src/lib/monaco-sanitizer.test.ts b/web/src/lib/monaco-sanitizer.test.ts new file mode 100644 index 00000000..a6fb29d0 --- /dev/null +++ b/web/src/lib/monaco-sanitizer.test.ts @@ -0,0 +1,21 @@ +import DOMPurify from "dompurify"; +// @ts-expect-error Monaco ships this runtime sanitizer without declarations. +import { safeSetInnerHtml, sanitizeHtml } from "monaco-editor/base/browser/domSanitize.js"; + +describe("Monaco sanitizer", () => { + it.each(["trusted HTML", "DOM fragment"])("uses the installed DOMPurify for %s", (output) => { + const sanitize = vi.spyOn(DOMPurify, "sanitize"); + const dirty = '

Kept formatting

' + + 'unsafe' + + 'safe'; + const target = document.createElement("div"); + + if (output === "trusted HTML") target.innerHTML = String(sanitizeHtml(dirty)); + else safeSetInnerHtml(target, dirty); + + expect(sanitize).toHaveBeenCalled(); + expect(target.querySelector("strong")).toHaveTextContent("formatting"); + expect(target.querySelector('script, [onerror], a[href^="javascript:"]')).toBeNull(); + expect(target.querySelector('a[href="https://example.com"]')).toHaveTextContent("safe"); + }); +}); diff --git a/web/src/v2/components/conversation.test.tsx b/web/src/v2/components/conversation.test.tsx index 007c5ebc..22c1f748 100644 --- a/web/src/v2/components/conversation.test.tsx +++ b/web/src/v2/components/conversation.test.tsx @@ -698,6 +698,34 @@ describe("V2Conversation", () => { expect(view.container.querySelector(".v2-conversation-scroll")!.scrollTop).toBe(180); }); + it("offers a return to the latest message while reading a completed chat without new content", async () => { + const view = renderConversation(baseClient()); + await screen.findByText("Title thread-a"); + await waitFor(() => expect(view.queryClient.getQueryState(v2QueryKeys.transcript("thread-a"))?.status).toBe("success")); + const scroller = view.container.querySelector(".v2-conversation-scroll")!; + let top = 1500; + Object.defineProperties(scroller, { scrollHeight: { value: 2000, configurable: true }, + clientHeight: { value: 500, configurable: true }, + scrollTop: { get: () => top, set: (value: number) => { top = Math.min(value, 1500); }, configurable: true } }); + fireEvent.scroll(scroller); + expect(screen.queryByRole("button", { name: "回到最新" })).not.toBeInTheDocument(); + scroller.scrollTop = 180; + fireEvent.scroll(scroller); + const jump = screen.getByRole("button", { name: "回到最新" }); + expect(screen.queryByRole("button", { name: "有新内容 · 回到最新" })).not.toBeInTheDocument(); + expect(scroller.scrollTop).toBe(180); + await userEvent.click(jump); + expect(scroller.scrollTop).toBe(1500); + expect(screen.queryByRole("button", { name: "回到最新" })).not.toBeInTheDocument(); + expect(view.queryClient.getQueryData(["v2", "reading", "thread-a"])).toEqual({ top: 1500, following: true }); + scroller.scrollTop = 120; + fireEvent.scroll(scroller); + expect(screen.getByRole("button", { name: "回到最新" })).toBeInTheDocument(); + scroller.scrollTop = 1500; + fireEvent.scroll(scroller); + expect(screen.queryByRole("button", { name: "回到最新" })).not.toBeInTheDocument(); + }); + it("follows viewport and card size changes without moving a reader looking at history", async () => { const resize = new Map void>(); vi.stubGlobal("ResizeObserver", class { diff --git a/web/src/v2/components/conversation.tsx b/web/src/v2/components/conversation.tsx index 25126fef..31ccb7df 100644 --- a/web/src/v2/components/conversation.tsx +++ b/web/src/v2/components/conversation.tsx @@ -568,6 +568,7 @@ export function V2Conversation({ client, threadID, workspaces, onArchive, onMana }, [threadID, messageID, returnToLatestMessage]); const scrollSizeRef = useRef<{ content: number; viewport: number } | null>(null); const [hasNewContent, setHasNewContent] = useState(false); + const [awayFromLatest, setAwayFromLatest] = useState(false); const optimistic = useMemo(() => submissions.filter(({ pending }) => pending).map(({ input }) => ({ id: input.operationKey, text: input.content, images: input.images, attachments: input.attachments, createdAt: input.createdAt, })), [submissions]); @@ -713,6 +714,7 @@ export function V2Conversation({ client, threadID, workspaces, onArchive, onMana element.scrollTop = saved && !saved.following ? saved.top : element.scrollHeight; scrollSizeRef.current = { content: element.scrollHeight, viewport: element.clientHeight }; setHasNewContent(false); + setAwayFromLatest(element.scrollHeight - element.clientHeight - element.scrollTop > 48); return; } const anchor = olderScrollAnchorRef.current; @@ -762,6 +764,7 @@ export function V2Conversation({ client, threadID, workspaces, onArchive, onMana if (readingThreadRef.current === threadID && followLatestRef.current && olderScrollAnchorRef.current?.threadID !== threadID) element.scrollTop = element.scrollHeight; scrollSizeRef.current = { content: element.scrollHeight, viewport: element.clientHeight }; + setAwayFromLatest(element.scrollHeight - element.clientHeight - element.scrollTop > 48); }); observer.observe(element); observer.observe(content); @@ -1105,6 +1108,7 @@ export function V2Conversation({ client, threadID, workspaces, onArchive, onMana scrollSizeRef.current = { content: element.scrollHeight, viewport: element.clientHeight }; const following = element.scrollHeight - element.clientHeight - element.scrollTop <= 48; followLatestRef.current = following; + setAwayFromLatest(!following); if (following) setHasNewContent(false); queryClient.setQueryData(["v2", "reading", threadID], { top: element.scrollTop, following }); reportReplyReading(); @@ -1180,12 +1184,16 @@ export function V2Conversation({ client, threadID, workspaces, onArchive, onMana } - {view === "conversation" && hasNewContent && } + }} type="button">{hasNewContent ? t("有新内容 · 回到最新", "New content · Jump to latest") : t("回到最新", "Jump to latest")}}
{(agentActivity || currentRun.status === "paused") &&
diff --git a/web/vite.config.ts b/web/vite.config.ts index 7faf222b..fd8574df 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -1,7 +1,23 @@ +import { fileURLToPath } from "node:url"; import react from "@vitejs/plugin-react"; -import { loadEnv } from "vite"; +import { loadEnv, normalizePath, type Plugin } from "vite"; import { defineConfig } from "vitest/config"; +// npm overrides cannot replace Monaco's embedded copy. Redirect its sanitizer import +// in both Vite and the separate dependency prebundler to the installed DOMPurify. +const monacoSanitizer = normalizePath(fileURLToPath(import.meta.resolve("monaco-editor/base/browser/domSanitize.js"))); +const monacoDOMPurify: Plugin = { + name: "monaco-dompurify", + enforce: "pre", + async resolveId(source, importer) { + if (source === "./dompurify/dompurify.js" && importer && normalizePath(importer) === monacoSanitizer) { + const resolved = await this.resolve("dompurify", importer, { skipSelf: true }); + if (!resolved) throw new Error("Cannot resolve installed DOMPurify for the Monaco sanitizer"); + return resolved; + } + }, +}; + export default defineConfig(({ mode }) => { const env = loadEnv(mode, ".", ""); const apiTarget = env.CYBERAGENT_API_TARGET || "http://127.0.0.1:8765"; @@ -19,7 +35,10 @@ export default defineConfig(({ mode }) => { }; return { - plugins: [react()], + plugins: [react(), monacoDOMPurify], + optimizeDeps: { + rolldownOptions: { plugins: [monacoDOMPurify] }, + }, server: { host: "127.0.0.1", port: 5173, @@ -39,6 +58,7 @@ export default defineConfig(({ mode }) => { }, test: { include: ["src/**/*.{test,spec}.{ts,tsx}"], + server: { deps: { inline: ["monaco-editor"] } }, environment: "jsdom", globals: true, setupFiles: "./src/test/setup.ts",