summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorRahul A Mistry <[email protected]>2026-01-25 05:59:58 +0530
committerGitHub <[email protected]>2026-01-24 18:29:58 -0600
commit399fec770f4f56c1df85a6f2d4858bc3f8c11c9a (patch)
treef25fad49b5b7842abf7f7d88ab76314e0da6d6ea
parent8d1a66d04333edda5288007cb7fbdae40d204d86 (diff)
downloadopencode-399fec770f4f56c1df85a6f2d4858bc3f8c11c9a.tar.gz
opencode-399fec770f4f56c1df85a6f2d4858bc3f8c11c9a.zip
fix(app): markdown rendering with morphdom for better dom functions (#10373)
-rw-r--r--bun.lock3
-rw-r--r--packages/ui/package.json1
-rw-r--r--packages/ui/src/components/markdown.tsx55
3 files changed, 53 insertions, 6 deletions
diff --git a/bun.lock b/bun.lock
index 34a6488ba..22002fcc0 100644
--- a/bun.lock
+++ b/bun.lock
@@ -423,6 +423,7 @@
"marked": "catalog:",
"marked-katex-extension": "5.1.6",
"marked-shiki": "catalog:",
+ "morphdom": "2.7.8",
"remeda": "catalog:",
"shiki": "catalog:",
"solid-js": "catalog:",
@@ -3102,6 +3103,8 @@
"mkdirp": ["[email protected]", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="],
+ "morphdom": ["[email protected]", "", {}, "sha512-D/fR4xgGUyVRbdMGU6Nejea1RFzYxYtyurG4Fbv2Fi/daKlWKuXGLOdXtl+3eIwL110cI2hz1ZojGICjjFLgTg=="],
+
"mrmime": ["[email protected]", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="],
"ms": ["[email protected]", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 20709f160..f384797f4 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -56,6 +56,7 @@
"marked": "catalog:",
"marked-katex-extension": "5.1.6",
"marked-shiki": "catalog:",
+ "morphdom": "2.7.8",
"remeda": "catalog:",
"shiki": "catalog:",
"solid-js": "catalog:",
diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx
index f7a1ec16f..e3102214b 100644
--- a/packages/ui/src/components/markdown.tsx
+++ b/packages/ui/src/components/markdown.tsx
@@ -1,6 +1,7 @@
import { useMarked } from "../context/marked"
import { useI18n } from "../context/i18n"
import DOMPurify from "dompurify"
+import morphdom from "morphdom"
import { checksum } from "@opencode-ai/util/encode"
import { ComponentProps, createEffect, createResource, createSignal, onCleanup, splitProps } from "solid-js"
import { isServer } from "solid-js/web"
@@ -194,18 +195,61 @@ export function Markdown(
{ initialValue: "" },
)
+ let copySetupTimer: ReturnType<typeof setTimeout> | undefined
+ let copyCleanup: (() => void) | undefined
+
createEffect(() => {
const container = root()
const content = html()
if (!container) return
- if (!content) return
if (isServer) return
- const cleanup = setupCodeCopy(container, {
- copy: i18n.t("ui.message.copy"),
- copied: i18n.t("ui.message.copied"),
+
+ if (!content) {
+ container.innerHTML = ""
+ return
+ }
+
+ const temp = document.createElement("div")
+ temp.innerHTML = content
+
+ morphdom(container, temp, {
+ childrenOnly: true,
+ onBeforeElUpdated: (fromEl, toEl) => {
+ if (fromEl.isEqualNode(toEl)) return false
+ if (fromEl.getAttribute("data-component") === "markdown-code") {
+ const fromPre = fromEl.querySelector("pre")
+ const toPre = toEl.querySelector("pre")
+ if (fromPre && toPre && !fromPre.isEqualNode(toPre)) {
+ morphdom(fromPre, toPre)
+ }
+ return false
+ }
+ return true
+ },
+ onBeforeNodeDiscarded: (node) => {
+ if (node instanceof Element) {
+ if (node.getAttribute("data-slot") === "markdown-copy-button") return false
+ if (node.getAttribute("data-component") === "markdown-code") return false
+ }
+ return true
+ },
})
- onCleanup(cleanup)
+
+ if (copySetupTimer) clearTimeout(copySetupTimer)
+ copySetupTimer = setTimeout(() => {
+ if (copyCleanup) copyCleanup()
+ copyCleanup = setupCodeCopy(container, {
+ copy: i18n.t("ui.message.copy"),
+ copied: i18n.t("ui.message.copied"),
+ })
+ }, 150)
})
+
+ onCleanup(() => {
+ if (copySetupTimer) clearTimeout(copySetupTimer)
+ if (copyCleanup) copyCleanup()
+ })
+
return (
<div
data-component="markdown"
@@ -213,7 +257,6 @@ export function Markdown(
...(local.classList ?? {}),
[local.class ?? ""]: !!local.class,
}}
- innerHTML={html.latest}
ref={setRoot}
{...others}
/>