diff --git a/src/components/markdown.tsx b/src/components/markdown.tsx
index a0db2f58..fccfe718 100644
--- a/src/components/markdown.tsx
+++ b/src/components/markdown.tsx
@@ -1,72 +1,70 @@
-import { createEffect, createSignal, Show } from "solid-js"
+import { createEffect, createSignal, onMount, onCleanup } from "solid-js"
import { renderMarkdown } from "../lib/markdown"
+import type { TextPart } from "../types/message"
interface MarkdownProps {
- content: string
+ part: TextPart
isDark?: boolean
}
export function Markdown(props: MarkdownProps) {
const [html, setHtml] = createSignal("")
let containerRef: HTMLDivElement | undefined
+ let latestRequestedText = ""
createEffect(async () => {
- const rendered = await renderMarkdown(props.content)
- setHtml(rendered)
+ const part = props.part
+ const text = part.text || ""
+
+ if (part.renderCache && part.renderCache.text === text) {
+ setHtml(part.renderCache.html)
+ return
+ }
+
+ latestRequestedText = text
+
+ try {
+ const rendered = await renderMarkdown(text)
+
+ if (latestRequestedText === text) {
+ setHtml(rendered)
+ part.renderCache = { text, html: rendered }
+ }
+ } catch (error) {
+ console.error("Failed to render markdown:", error)
+ if (latestRequestedText === text) {
+ setHtml(text)
+ }
+ }
})
- createEffect(() => {
- const currentHtml = html()
- if (containerRef && currentHtml) {
- setTimeout(() => {
- const codeBlocks = containerRef?.querySelectorAll(".markdown-code-block")
+ onMount(() => {
+ const handleClick = async (e: Event) => {
+ const target = e.target as HTMLElement
+ const copyButton = target.closest(".code-block-copy") as HTMLButtonElement
- codeBlocks?.forEach((block) => {
- const existing = block.querySelector(".code-block-header")
- if (existing) return
-
- const lang = block.getAttribute("data-language")
- const encodedCode = block.getAttribute("data-code")
-
- const header = document.createElement("div")
- header.className = "code-block-header"
-
- const languageSpan = lang
- ? `${lang}`
- : ''
-
- header.innerHTML = `
- ${languageSpan}
-
- `
- block.insertBefore(header, block.firstChild)
-
- const button = header.querySelector(".code-block-copy")
- if (button) {
- button.addEventListener("click", async () => {
- const code = button.getAttribute("data-code")
- if (code) {
- const decodedCode = decodeURIComponent(code)
- await navigator.clipboard.writeText(decodedCode)
- const copyText = button.querySelector(".copy-text")
- if (copyText) {
- copyText.textContent = "Copied!"
- setTimeout(() => {
- copyText.textContent = "Copy"
- }, 2000)
- }
- }
- })
+ if (copyButton) {
+ e.preventDefault()
+ const code = copyButton.getAttribute("data-code")
+ if (code) {
+ const decodedCode = decodeURIComponent(code)
+ await navigator.clipboard.writeText(decodedCode)
+ const copyText = copyButton.querySelector(".copy-text")
+ if (copyText) {
+ copyText.textContent = "Copied!"
+ setTimeout(() => {
+ copyText.textContent = "Copy"
+ }, 2000)
}
- })
- }, 0)
+ }
+ }
}
+
+ containerRef?.addEventListener("click", handleClick)
+
+ onCleanup(() => {
+ containerRef?.removeEventListener("click", handleClick)
+ })
})
return
diff --git a/src/components/message-part.tsx b/src/components/message-part.tsx
index 521ab99b..fe4535e3 100644
--- a/src/components/message-part.tsx
+++ b/src/components/message-part.tsx
@@ -25,7 +25,7 @@ export default function MessagePart(props: MessagePartProps) {
-
+
@@ -48,7 +48,7 @@ export default function MessagePart(props: MessagePartProps) {
-
+
diff --git a/src/components/tool-call.tsx b/src/components/tool-call.tsx
index cd6e2d29..8a5a0109 100644
--- a/src/components/tool-call.tsx
+++ b/src/components/tool-call.tsx
@@ -357,7 +357,7 @@ export default function ToolCall(props: ToolCallProps) {
return (
)
@@ -384,7 +384,7 @@ export default function ToolCall(props: ToolCallProps) {
if (hasMarkdownCodeBlocks(truncated)) {
return (
-
+
)
}
@@ -479,7 +479,7 @@ export default function ToolCall(props: ToolCallProps) {
if (hasMarkdownCodeBlocks(truncated)) {
return (
-
+
)
}
diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts
index 2899720c..191056a1 100644
--- a/src/lib/markdown.ts
+++ b/src/lib/markdown.ts
@@ -1,6 +1,27 @@
import { marked } from "marked"
import { createHighlighter, type Highlighter, bundledLanguages } from "shiki/bundle/full"
+const CORE_LANGUAGES = [
+ "bash",
+ "shell",
+ "sh",
+ "javascript",
+ "typescript",
+ "tsx",
+ "jsx",
+ "json",
+ "yaml",
+ "yml",
+ "markdown",
+ "md",
+ "html",
+ "css",
+ "scss",
+ "python",
+ "go",
+ "rust",
+]
+
let highlighter: Highlighter | null = null
let highlighterPromise: Promise | null = null
let currentTheme: "light" | "dark" = "light"
@@ -15,9 +36,11 @@ async function getOrCreateHighlighter() {
return highlighterPromise
}
+ const filteredLangs = CORE_LANGUAGES.filter((lang) => lang in bundledLanguages)
+
highlighterPromise = createHighlighter({
themes: ["github-light", "github-dark"],
- langs: Object.keys(bundledLanguages),
+ langs: filteredLangs,
})
highlighter = await highlighterPromise
@@ -41,8 +64,21 @@ function setupRenderer(isDark: boolean) {
const encodedCode = encodeURIComponent(code)
const escapedLang = lang ? escapeHtml(lang) : ""
+ const header = `
+
+ `
+
if (!lang || !highlighter) {
- return ``
+ return `${header}
${escapeHtml(code)}
`
}
try {
@@ -50,9 +86,9 @@ function setupRenderer(isDark: boolean) {
lang,
theme: isDark ? "github-dark" : "github-light",
})
- return `${html}
`
+ return `${header}${html}
`
} catch {
- return ``
+ return `${header}
${escapeHtml(code)}
`
}
}
diff --git a/src/stores/sessions.ts b/src/stores/sessions.ts
index 1fddfae0..6a50ef80 100644
--- a/src/stores/sessions.ts
+++ b/src/stores/sessions.ts
@@ -706,11 +706,19 @@ async function loadMessages(instanceId: string, sessionId: string, force = false
messagesInfo.set(messageId, info)
+ // Clear render cache for all parts when loading messages
+ const parts = (apiMessage.parts || []).map((part: any) => {
+ if (part.type === "text") {
+ return { ...part, renderCache: undefined }
+ }
+ return part
+ })
+
const message: Message = {
id: messageId,
sessionId,
type: role === "user" ? "user" : "assistant",
- parts: apiMessage.parts || [],
+ parts,
timestamp: info.time?.created || Date.now(),
status: "complete" as const,
version: 0,
@@ -855,12 +863,24 @@ function handleMessageUpdate(instanceId: string, event: any): void {
if (message.parts.some((partItem: any) => partItem.synthetic === true)) {
message.parts = message.parts.filter((partItem: any) => partItem.synthetic !== true)
filteredSynthetics = true
+ // Clear render cache from remaining parts when synthetic parts are removed
+ message.parts.forEach((partItem: any) => {
+ if (partItem.type === "text") {
+ partItem.renderCache = undefined
+ }
+ })
}
let baseParts: any[]
if (replacedTemp) {
baseParts = message.parts.filter((partItem: any) => partItem.type !== "text")
message.parts = baseParts
+ // Clear render cache when replacing temp content
+ baseParts.forEach((partItem: any) => {
+ if (partItem.type === "text") {
+ partItem.renderCache = undefined
+ }
+ })
} else {
baseParts = message.parts
}
@@ -881,6 +901,10 @@ function handleMessageUpdate(instanceId: string, event: any): void {
partMap.set(part.id, baseParts.length - 1)
}
shouldIncrementVersion = true
+ // Clear render cache for new text parts
+ if (part.type === "text") {
+ part.renderCache = undefined
+ }
} else {
const previousPart = baseParts[partIndex]
const textUnchanged =
@@ -897,6 +921,10 @@ function handleMessageUpdate(instanceId: string, event: any): void {
baseParts[partIndex] = part
if (part.type !== "text" || !previousPart || previousPart.text !== part.text) {
shouldIncrementVersion = true
+ // Clear render cache when text changes
+ if (part.type === "text") {
+ part.renderCache = undefined
+ }
}
}
@@ -1147,6 +1175,7 @@ async function sendMessage(
type: "text" as const,
text: prompt,
synthetic: true,
+ renderCache: undefined,
},
]
@@ -1208,6 +1237,7 @@ async function sendMessage(
type: "text" as const,
text: source.value,
synthetic: true,
+ renderCache: undefined,
})
}
}
diff --git a/src/types/message.ts b/src/types/message.ts
index c2a54ec8..51b47914 100644
--- a/src/types/message.ts
+++ b/src/types/message.ts
@@ -1,3 +1,8 @@
+export interface RenderCache {
+ text: string
+ html: string
+}
+
export interface MessageDisplayParts {
text: any[]
tool: any[]
@@ -17,3 +22,11 @@ export interface Message {
version: number
displayParts?: MessageDisplayParts
}
+
+export interface TextPart {
+ id?: string
+ type: "text"
+ text: string
+ synthetic?: boolean
+ renderCache?: RenderCache
+}