fix(app): move markdown parsing to worker (#40356)

This commit is contained in:
Luke Parker
2026-08-04 14:13:34 +10:00
committed by GitHub
parent 7b265e3617
commit 405feb7cb1
25 changed files with 826 additions and 705 deletions
@@ -1,6 +1,6 @@
import { checksum } from "@opencode-ai/core/util/encode"
import DOMPurify from "dompurify"
import { project } from "./markdown-stream"
import { parseMarkdown } from "./markdown-worker"
export type MarkdownCacheEntry = {
raw: string
@@ -52,27 +52,18 @@ export function touchCachedMarkdown(key: string, value: MarkdownCacheEntry) {
cache.delete(first)
}
export async function preloadMarkdown(
text: string,
cacheKey: string,
parser: { parse(text: string): string | Promise<string> },
) {
await Promise.all(
project(undefined, text, false).blocks.map(async (block, index) => {
if (block.mode === "code") return
const key = `${cacheKey}:${index}:${block.mode}`
const cached = getCachedMarkdown(key)
if (cached?.raw === block.raw) {
touchCachedMarkdown(key, cached)
return
}
const hash = checksum(block.raw)
if (!hash) return
touchCachedMarkdown(key, {
raw: block.raw,
hash,
html: sanitizeMarkdown(await Promise.resolve(parser.parse(block.src))),
})
}),
)
export async function preloadMarkdown(text: string, cacheKey: string) {
const key = `${cacheKey}:0:full`
const cached = getCachedMarkdown(key)
if (cached?.raw === text) {
touchCachedMarkdown(key, cached)
return
}
const hash = checksum(text)
if (!hash) return
touchCachedMarkdown(key, {
raw: text,
hash,
html: sanitizeMarkdown(await parseMarkdown(text)),
})
}
@@ -1,18 +0,0 @@
import { expect, test } from "bun:test"
import { preloadMarkdown } from "./markdown-cache"
test("preloads completed markdown into the render cache", async () => {
const parsed: string[] = []
const parser = {
parse(text: string) {
parsed.push(text)
return `<p>${text}</p>`
},
}
const key = `markdown-preload-${crypto.randomUUID()}`
await preloadMarkdown("prepared response", key, parser)
await preloadMarkdown("prepared response", key, parser)
expect(parsed).toEqual(["prepared response"])
})
@@ -0,0 +1,11 @@
import type { Block, Projection } from "./markdown-stream"
export function completedProjection(text: string): Projection {
return { text, blocks: [{ raw: text, src: text, mode: "full" }] }
}
export function canReusePendingBlock(current: Pick<Block, "mode" | "raw"> | undefined, next: Block) {
if (!current || current.mode !== next.mode) return false
if (next.mode === "code" || next.mode === "live") return next.raw.startsWith(current.raw)
return current.raw === next.raw
}
@@ -1,5 +1,6 @@
import { describe, expect, test } from "bun:test"
import { canReusePendingBlock, project, stream } from "./markdown-stream"
import { canReusePendingBlock } from "./markdown-projection"
import { project, stream } from "./markdown-stream"
describe("markdown stream", () => {
test("heals incomplete emphasis while streaming", () => {
@@ -129,6 +130,9 @@ describe("markdown stream", () => {
expect(
canReusePendingBlock({ mode: "code", raw: "```ts\none" }, { mode: "code", raw: "```ts\none two", src: "" }),
).toBe(true)
expect(canReusePendingBlock({ mode: "live", raw: "partial" }, { mode: "live", raw: "partial text", src: "" })).toBe(
true,
)
expect(canReusePendingBlock({ mode: "code", raw: "```ts\none" }, { mode: "live", raw: "one", src: "" })).toBe(false)
})
@@ -145,6 +149,39 @@ describe("markdown stream", () => {
})
})
test("finalizes only the live tail when streaming stops", () => {
const live = project(undefined, "# Plan\n\nFinished paragraph.\n\n- final item", true)
const final = project(live, live.text, false)
expect(final.blocks[0]).toBe(live.blocks[0])
expect(final.blocks[1]).toBe(live.blocks[1])
expect(final.blocks[2]).toEqual({ raw: "- final item", src: "- final item", mode: "full" })
})
test("catches up paced text before finalizing", () => {
const live = project(undefined, "# Plan\n\nFinished paragraph.\n\n- final", true)
const final = project(live, `${live.text} item`, false)
expect(canReusePendingBlock(live.blocks[0], final.blocks[0]!)).toBe(true)
expect(canReusePendingBlock(live.blocks[1], final.blocks[1]!)).toBe(true)
expect(final.blocks[2]).toEqual({ raw: "- final item", src: "- final item", mode: "full" })
})
test("completes an open code block when streaming stops", () => {
const live = project(undefined, "```ts\nconst value = 1", true)
const final = project(live, live.text, false)
expect(final.blocks).toEqual([
{
raw: "```ts\nconst value = 1",
src: "const value = 1",
mode: "code",
language: "ts",
complete: true,
},
])
})
test("does not add a blank line before the first streamed code", () => {
const previous = project(undefined, "```ts\n", true)
const next = project(previous, `${previous.text}const x = 1`, true)
@@ -1,5 +1,6 @@
import { marked, type Tokens } from "marked"
import remend from "remend"
import { completedProjection } from "./markdown-projection"
export type Block = {
raw: string
@@ -50,7 +51,7 @@ function heal(text: string) {
}
export function stream(text: string, live: boolean): Block[] {
if (!live) return [{ raw: text, src: text, mode: "full" }] satisfies Block[]
if (!live) return completedProjection(text).blocks
if (refs(text)) return [{ raw: text, src: heal(text), mode: "live" }] satisfies Block[]
const tokens = marked.lexer(text)
const tail = tokens.findLastIndex((token) => token.type !== "space")
@@ -84,14 +85,25 @@ export function stream(text: string, live: boolean): Block[] {
return [...result, { raw, src: openCode(code.raw), mode: "code", language: language(code.lang) }]
}
export function canReusePendingBlock(current: Pick<Block, "mode" | "raw"> | undefined, next: Block) {
if (!current || current.mode !== next.mode) return false
if (next.mode === "code") return next.raw.startsWith(current.raw)
return current.raw === next.raw
}
export function project(previous: Projection | undefined, text: string, live: boolean): Projection {
if (!live || !previous || !text.startsWith(previous.text)) return { text, blocks: stream(text, live) }
if (!live) {
const current =
previous?.text === text
? previous
: previous && text.startsWith(previous.text)
? project(previous, text, true)
: undefined
if (!current) return completedProjection(text)
return {
text,
blocks: current.blocks.map((block) => {
if (block.mode === "live") return { raw: block.raw, src: block.raw, mode: "full" }
if (block.mode === "code" && !block.complete) return { ...block, complete: true }
return block
}),
}
}
if (!previous || !text.startsWith(previous.text)) return { text, blocks: stream(text, live) }
const tail = previous.blocks.at(-1)
const suffix = text.slice(previous.text.length)
if (!suffix || tail?.mode !== "code" || tail.complete || closesFence(tail.raw, suffix))
@@ -10,6 +10,7 @@ const response = (id: number, reset: boolean, stable: [string, string][], unstab
type: "highlight" as const,
id,
key: "code",
language: "typescript",
reset,
stable,
unstable,
@@ -20,6 +21,7 @@ test("accumulates stable worker tokens and replaces the unstable tail", () => {
type: "highlight",
id: 1,
key: "code",
language: "typescript",
reset: true,
stable: [token("one\n")],
unstable: [token("tw")],
@@ -28,6 +30,7 @@ test("accumulates stable worker tokens and replaces the unstable tail", () => {
type: "highlight",
id: 2,
key: "code",
language: "typescript",
reset: false,
stable: [token("two\n")],
unstable: [token("three")],
@@ -35,6 +38,7 @@ test("accumulates stable worker tokens and replaces the unstable tail", () => {
expect(second.stable.map((item) => item[0])).toEqual(["one\n", "two\n"])
expect(second.unstable.map((item) => item[0])).toEqual(["three"])
expect(second.language).toBe("typescript")
})
test("increments generation only when the worker resets token identity", () => {
@@ -45,12 +49,13 @@ test("increments generation only when the worker resets token identity", () => {
})
test("ignores stale worker responses and resets replacement streams", () => {
const current = { id: 2, generation: 1, stable: [token("current")], unstable: [] }
const current = { id: 2, generation: 1, language: "typescript", stable: [token("current")], unstable: [] }
expect(
applyMarkdownWorkerResponse(current, {
type: "highlight",
id: 1,
key: "code",
language: "typescript",
reset: false,
stable: [token("stale")],
unstable: [],
@@ -62,6 +67,7 @@ test("ignores stale worker responses and resets replacement streams", () => {
type: "highlight",
id: 3,
key: "code",
language: "typescript",
reset: true,
stable: [token("replacement")],
unstable: [],
@@ -1,27 +1,32 @@
import type { ThemeRegistrationResolved } from "shiki"
import type { Projection } from "./markdown-stream"
export type MarkdownToken = [content: string, style: string]
export type MarkdownWorkerRequest =
| { type: "init"; theme: ThemeRegistrationResolved }
| { type: "parse"; id: number; text: string }
| { type: "project"; id: number; key: string; text: string; live: boolean }
| { type: "highlight"; id: number; key: string; text: string; language: string; complete?: boolean }
| { type: "dispose"; key: string }
export type MarkdownWorkerResponse =
| { type: "parse"; id: number; html: string }
| { type: "project"; id: number; key: string; projection: Projection }
| {
type: "highlight"
id: number
key: string
language: string
reset: boolean
stable: MarkdownToken[]
unstable: MarkdownToken[]
}
| { type: "error"; id: number; key: string; message: string }
| { type: "error"; id: number; key?: string; message: string }
| { type: "superseded"; id: number; key: string }
export type MarkdownWorkerState = {
id: number
generation: number
language: string
stable: MarkdownToken[]
unstable: MarkdownToken[]
}
@@ -42,6 +47,7 @@ export function applyMarkdownWorkerResponse(
return {
id: response.id,
generation: (state?.generation ?? 0) + (response.reset ? 1 : 0),
language: response.language,
stable: response.reset ? response.stable : [...(state?.stable ?? []), ...response.stable],
unstable: response.unstable,
}
@@ -1,5 +1,4 @@
import MarkdownShikiWorkerUrl from "./markdown-shiki.worker.ts?worker&url"
import { OpenCodeTheme } from "@opencode-ai/ui/context/marked"
import MarkdownWorkerUrl from "./markdown.worker.ts?worker&url"
import {
applyMarkdownWorkerResponse,
shouldReleaseMarkdownWorkerState,
@@ -8,18 +7,32 @@ import {
type MarkdownWorkerState,
} from "./markdown-worker-protocol"
import { createWorkerTransport } from "./markdown-worker-transport"
import type { Projection } from "./markdown-stream"
type Pending = {
type HighlightPending = {
key: string
complete: boolean
resolve: (state: MarkdownWorkerState) => void
reject: (error: Error) => void
}
type ProjectPending = {
key: string
resolve: (projection: Projection) => void
reject: (error: Error) => void
}
type ParsePending = {
resolve: (html: string) => void
reject: (error: Error) => void
}
let worker: Worker | undefined
let disabled: Error | undefined
let nextID = 0
const pending = new Map<number, Pending>()
const pending = new Map<number, HighlightPending>()
const projects = new Map<number, ProjectPending>()
const parses = new Map<number, ParsePending>()
const states = new Map<string, MarkdownWorkerState>()
const keys = new Set<string>()
const latest = new Map<string, number>()
@@ -32,6 +45,43 @@ const transport = createWorkerTransport<Extract<MarkdownWorkerRequest, { type: "
result.reject(new MarkdownWorkerSupersededError())
},
})
const projectTransport = createWorkerTransport<Extract<MarkdownWorkerRequest, { type: "project" }>>({
post: (request) => worker!.postMessage(request),
supersede: (request) => {
const result = projects.get(request.id)
if (!result) return
projects.delete(request.id)
result.reject(new MarkdownWorkerSupersededError())
},
})
export function parseMarkdown(text: string) {
const instance = getWorker()
const id = ++nextID
return new Promise<string>((resolve, reject) => {
parses.set(id, { resolve, reject })
instance.postMessage({ type: "parse", id, text } satisfies MarkdownWorkerRequest)
})
}
export function projectMarkdown(key: string, text: string, live: boolean) {
getWorker()
const id = ++nextID
return new Promise<Projection>((resolve, reject) => {
projects.set(id, { key, resolve, reject })
projectTransport.send({ type: "project", id, key, text, live })
})
}
export function disposeMarkdownProjection(key: string) {
projectTransport.dispose(key)
projects.forEach((request, id) => {
if (request.key !== key) return
projects.delete(id)
request.reject(new MarkdownWorkerDisposedError())
})
worker?.postMessage({ type: "dispose", key } satisfies MarkdownWorkerRequest)
}
export function highlightStreamingCode(key: string, text: string, language: string, complete = false) {
const instance = getWorker()
@@ -67,48 +117,97 @@ function getWorker() {
if (worker) return worker
if (disabled) throw new MarkdownWorkerUnavailableError(disabled.message)
try {
worker = new Worker(MarkdownShikiWorkerUrl, { type: "module" })
worker = new Worker(MarkdownWorkerUrl, { type: "module" })
} catch (error) {
disabled = error instanceof Error ? error : new Error(String(error))
throw new MarkdownWorkerUnavailableError(disabled.message)
}
worker.onmessage = (event: MessageEvent<MarkdownWorkerResponse>) => {
if (event.data.type === "parse") {
const result = parses.get(event.data.id)
if (!result) return
parses.delete(event.data.id)
result.resolve(event.data.html)
return
}
if (event.data.type === "project") {
const result = projects.get(event.data.id)
if (!result) {
projectTransport.complete(event.data.key, event.data.id)
return
}
projects.delete(event.data.id)
result.resolve(event.data.projection)
projectTransport.complete(event.data.key, event.data.id)
return
}
if (event.data.type === "error") {
const parsed = parses.get(event.data.id)
if (parsed) {
parses.delete(event.data.id)
parsed.reject(new Error(event.data.message))
return
}
const projected = projects.get(event.data.id)
if (projected) {
projects.delete(event.data.id)
projected.reject(new Error(event.data.message))
projectTransport.complete(projected.key, event.data.id)
return
}
}
if (event.data.type === "superseded") {
const projected = projects.get(event.data.id)
if (projected) {
projects.delete(event.data.id)
projected.reject(new MarkdownWorkerSupersededError())
projectTransport.complete(projected.key, event.data.id)
return
}
}
const key = event.data.key
if (!key) return
const result = pending.get(event.data.id)
if (!result) {
transport.complete(event.data.key, event.data.id)
transport.complete(key, event.data.id)
return
}
pending.delete(event.data.id)
if (!keys.has(event.data.key)) {
if (!keys.has(key)) {
result.reject(new MarkdownWorkerDisposedError())
transport.complete(event.data.key, event.data.id)
transport.complete(key, event.data.id)
return
}
if (event.data.type === "superseded") {
result.reject(new MarkdownWorkerSupersededError())
transport.complete(event.data.key, event.data.id)
transport.complete(key, event.data.id)
return
}
if (event.data.type === "error") {
result.reject(new Error(event.data.message))
transport.complete(event.data.key, event.data.id)
transport.complete(key, event.data.id)
return
}
const state = applyMarkdownWorkerResponse(states.get(event.data.key), event.data)
if (shouldReleaseMarkdownWorkerState(result.complete, latest.get(event.data.key), event.data.id)) {
states.delete(event.data.key)
keys.delete(event.data.key)
latest.delete(event.data.key)
} else states.set(event.data.key, state)
const state = applyMarkdownWorkerResponse(states.get(key), event.data)
if (shouldReleaseMarkdownWorkerState(result.complete, latest.get(key), event.data.id)) {
states.delete(key)
keys.delete(key)
latest.delete(key)
} else states.set(key, state)
result.resolve(state)
transport.complete(event.data.key, event.data.id)
transport.complete(key, event.data.id)
}
const fail = (message: string) => {
const error = new Error(message)
disabled = error
transport.reset()
projectTransport.reset()
pending.forEach((request) => request.reject(error))
projects.forEach((request) => request.reject(error))
parses.forEach((request) => request.reject(error))
pending.clear()
projects.clear()
parses.clear()
states.clear()
keys.clear()
latest.clear()
@@ -117,6 +216,5 @@ function getWorker() {
}
worker.onerror = (event) => fail(event.message || "Markdown highlighting worker failed")
worker.onmessageerror = () => fail("Markdown worker response failed")
worker.postMessage({ type: "init", theme: OpenCodeTheme } satisfies MarkdownWorkerRequest)
return worker
}
+53 -15
View File
@@ -1,4 +1,3 @@
import { useMarked } from "@opencode-ai/ui/context/marked"
import { useI18n } from "@opencode-ai/ui/context/i18n"
import morphdom from "morphdom"
import { checksum } from "@opencode-ai/core/util/encode"
@@ -6,7 +5,6 @@ import {
type Accessor,
type ComponentProps,
createEffect,
createMemo,
createResource,
createSignal,
createUniqueId,
@@ -18,14 +16,17 @@ import { isServer, render } from "solid-js/web"
import { Icon as IconV2 } from "@opencode-ai/ui/v2/icon"
import { IconButtonV2 } from "@opencode-ai/ui/v2/icon-button-v2"
import { TooltipV2 } from "@opencode-ai/ui/v2/tooltip-v2"
import { bundledLanguages } from "shiki"
import { canReusePendingBlock, project, type Block, type Projection } from "./markdown-stream"
import { canReusePendingBlock, completedProjection } from "./markdown-projection"
import type { Block, Projection } from "./markdown-stream"
import {
disposeMarkdownProjection,
disposeStreamingCode,
highlightStreamingCode,
MarkdownWorkerDisposedError,
MarkdownWorkerSupersededError,
MarkdownWorkerUnavailableError,
parseMarkdown,
projectMarkdown,
} from "./markdown-worker"
import { markdownBlockKey, type MarkdownToken } from "./markdown-worker-protocol"
import { shouldResetCodeTokens, type RenderedCodeState } from "./markdown-code-state"
@@ -67,10 +68,14 @@ function fallback(markdown: string) {
}
async function code(text: string, language: string | undefined, key: string, complete = false) {
const name = language && language in bundledLanguages ? language : "text"
try {
const result = await highlightStreamingCode(key, text, name, complete)
return { language: name, generation: result.generation, stable: result.stable, unstable: result.unstable }
const result = await highlightStreamingCode(key, text, language ?? "text", complete)
return {
language: result.language,
generation: result.generation,
stable: result.stable,
unstable: result.unstable,
}
} catch (error) {
if (
!(error instanceof MarkdownWorkerDisposedError) &&
@@ -78,7 +83,7 @@ async function code(text: string, language: string | undefined, key: string, com
!(error instanceof MarkdownWorkerUnavailableError)
)
console.error("Markdown highlighting worker failed", error)
return { language: name, generation: 0, stable: [], unstable: [[text, ""] as MarkdownToken] }
return { language: language ?? "text", generation: 0, stable: [], unstable: [[text, ""] as MarkdownToken] }
}
}
@@ -352,6 +357,10 @@ function initialResult(text: string, key: string | undefined, projection: Projec
}
}
function pendingProjection(text: string): Projection {
return { text, blocks: text ? [{ raw: text, src: text, mode: "live" }] : [] }
}
export function Markdown(
props: ComponentProps<"div"> & {
text: string
@@ -362,21 +371,44 @@ export function Markdown(
},
) {
const [local, others] = splitProps(props, ["text", "cacheKey", "streaming", "class", "classList"])
const marked = useMarked()
const i18n = useI18n()
const [root, setRoot] = createSignal<HTMLDivElement>()
const owner = createUniqueId()
const activeCodeKeys = new Set<string>()
const completedCode = new Map<string, Extract<RenderedBlock, { mode: "code" }>>()
const projection = createMemo((previous: Projection | undefined) =>
project(previous, local.text, local.streaming ?? false),
let streamed = false
const [projection] = createResource(
() => {
if (isServer) return
const live = local.streaming ?? false
if (live) streamed = true
if (!live && !streamed) return
return { key: owner, text: local.text, live }
},
(src) => projectMarkdown(src.key, src.text, src.live),
{ initialValue: pendingProjection("") },
)
const currentProjection = () => {
if (!(local.streaming ?? false) && !streamed) return completedProjection(local.text)
const value = projection.latest
if (value?.text === local.text) return value
if (value?.text) return value
return pendingProjection(local.text)
}
const [html] = createResource(
() => {
if (isServer)
return {
text: local.text,
key: local.cacheKey,
projection: pendingProjection(local.text),
}
const value = !(local.streaming ?? false) && !streamed ? completedProjection(local.text) : projection.latest
if (!value || value.text !== local.text) return
return {
text: local.text,
key: local.cacheKey,
projection: projection(),
projection: value,
}
},
async (src) => {
@@ -426,7 +458,7 @@ export function Markdown(
}
const hash = checksum(block.raw)
const safe = sanitizeMarkdown(await Promise.resolve(marked.parse(block.src)))
const safe = sanitizeMarkdown(await parseMarkdown(block.src))
if (key && hash) touchCachedMarkdown(key, { raw: block.raw, hash, html: safe })
return { key: blockKey, mode: block.mode, raw: block.raw, hash: hash ?? "", html: safe }
}),
@@ -449,7 +481,12 @@ export function Markdown(
)
},
{
initialValue: initialResult(local.text, local.cacheKey, projection(), owner),
initialValue: initialResult(
local.text,
local.cacheKey,
local.streaming ? pendingProjection(local.text) : completedProjection(local.text),
owner,
),
},
)
@@ -458,7 +495,7 @@ export function Markdown(
createEffect(() => {
const container = root()
const result = html.latest ?? html()
const projected = projection()
const projected = currentProjection()
const content = local.text ? pendingBlocks(result, projected, local.cacheKey, owner) : []
if (!container) return
if (isServer) return
@@ -497,6 +534,7 @@ export function Markdown(
onCleanup(() => {
if (copyCleanup) copyCleanup()
disposeMarkdownProjection(owner)
activeCodeKeys.forEach(disposeCode)
completedCode.clear()
})
@@ -1,6 +1,8 @@
/// <reference lib="webworker" />
import { ShikiStreamTokenizer } from "@shikijs/stream"
import { createMarkdownParser } from "@opencode-ai/ui/context/marked-parser"
import { OpenCodeTheme } from "@opencode-ai/ui/context/marked-theme"
import {
bundledLanguages,
createHighlighter,
@@ -11,6 +13,7 @@ import {
} from "shiki"
import type { MarkdownToken, MarkdownWorkerRequest, MarkdownWorkerResponse } from "./markdown-worker-protocol"
import { createLatestWorkerQueue } from "./markdown-worker-queue"
import { project, type Projection } from "./markdown-stream"
type Stream = {
language: string
@@ -19,30 +22,70 @@ type Stream = {
}
const streams = new Map<string, Stream>()
const projections = new Map<string, Projection>()
let highlighter: ReturnType<typeof createHighlighter> | undefined
const queue = createLatestWorkerQueue<Extract<MarkdownWorkerRequest, { type: "highlight" }>>({
const highlightQueue = createLatestWorkerQueue<Extract<MarkdownWorkerRequest, { type: "highlight" }>>({
run: highlight,
supersede: (request) => post({ type: "superseded", id: request.id, key: request.key }),
dispose: (key) => void streams.delete(key),
})
const projectQueue = createLatestWorkerQueue<Extract<MarkdownWorkerRequest, { type: "project" }>>({
run: runProject,
supersede: (request) => post({ type: "superseded", id: request.id, key: request.key }),
dispose: (key) => void projections.delete(key),
})
const parser = createMarkdownParser(async (code, language) => {
const instance = await getHighlighter()
const name = language in bundledLanguages ? language : "text"
if (!instance.getLoadedLanguages().includes(name))
await instance.loadLanguage(bundledLanguages[name as BundledLanguage])
return instance.codeToHtml(code, { lang: name as BundledLanguage, theme: "OpenCode", tabindex: false })
})
self.onmessage = (event: MessageEvent<MarkdownWorkerRequest>) => {
if (event.data.type === "init") {
highlighter ??= createHighlighter({ themes: [event.data.theme], langs: [] })
if (event.data.type === "dispose") {
highlightQueue.dispose(event.data.key)
projectQueue.dispose(event.data.key)
return
}
if (event.data.type === "dispose") {
queue.dispose(event.data.key)
if (event.data.type === "parse") {
void parse(event.data)
return
}
if (event.data.type === "project") {
projectQueue.highlight(event.data)
return
}
queue.highlight(event.data)
highlightQueue.highlight(event.data)
}
async function parse(request: Extract<MarkdownWorkerRequest, { type: "parse" }>) {
try {
post({ type: "parse", id: request.id, html: await parser.parse(request.text) })
} catch (error) {
post({ type: "error", id: request.id, message: error instanceof Error ? error.message : String(error) })
}
}
async function runProject(request: Extract<MarkdownWorkerRequest, { type: "project" }>) {
try {
const projection = project(projections.get(request.key), request.text, request.live)
projections.set(request.key, projection)
post({ type: "project", id: request.id, key: request.key, projection })
} catch (error) {
post({
type: "error",
id: request.id,
key: request.key,
message: error instanceof Error ? error.message : String(error),
})
}
}
async function highlight(request: Extract<MarkdownWorkerRequest, { type: "highlight" }>) {
try {
const instance = await highlighter
if (!instance) throw new Error("Shiki worker is not initialized")
const instance = await getHighlighter()
const language = request.language in bundledLanguages ? request.language : "text"
if (!instance.getLoadedLanguages().includes(language))
await instance.loadLanguage(bundledLanguages[language as BundledLanguage])
@@ -54,6 +97,7 @@ async function highlight(request: Extract<MarkdownWorkerRequest, { type: "highli
type: "highlight",
id: request.id,
key: request.key,
language,
reset: true,
stable: result.tokens
.flatMap((line, index) =>
@@ -81,6 +125,7 @@ async function highlight(request: Extract<MarkdownWorkerRequest, { type: "highli
type: "highlight",
id: request.id,
key: request.key,
language,
reset,
stable: result.stable.filter((token) => token.content.length > 0).map(token),
unstable: result.unstable.filter((token) => token.content.length > 0).map(token),
@@ -95,6 +140,10 @@ async function highlight(request: Extract<MarkdownWorkerRequest, { type: "highli
}
}
function getHighlighter() {
return (highlighter ??= createHighlighter({ themes: [OpenCodeTheme], langs: [] }))
}
function post(response: MarkdownWorkerResponse) {
self.postMessage(response)
}
@@ -1739,9 +1739,7 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
<Show when={text()}>
<div data-component="text-part" data-timeline-part-id={part().id}>
<div data-slot="text-part-body">
<Show when={streaming()} fallback={<Markdown text={text()} cacheKey={part().id} streaming={false} />}>
<PacedMarkdown text={text()} cacheKey={part().id} streaming={streaming()} />
</Show>
<PacedMarkdown text={text()} cacheKey={part().id} streaming={streaming()} />
</div>
<Show when={showCopy()}>
<div data-slot="text-part-copy-wrapper" data-interrupted={interrupted() ? "" : undefined}>
@@ -1776,9 +1774,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) {
return (
<Show when={text()}>
<div data-component="reasoning-part" data-timeline-part-id={part().id}>
<Show when={streaming()} fallback={<Markdown text={text()} cacheKey={part().id} streaming={false} />}>
<PacedMarkdown text={text()} cacheKey={part().id} streaming={streaming()} />
</Show>
<PacedMarkdown text={text()} cacheKey={part().id} streaming={streaming()} />
</div>
</Show>
)
+3
View File
@@ -1,5 +1,8 @@
import { WorkerPoolManager } from "@pierre/diffs/worker"
import ShikiWorkerUrl from "@pierre/diffs/worker/worker.js?worker&url"
import { registerOpenCodeTheme } from "@opencode-ai/ui/context/marked-theme-register"
registerOpenCodeTheme()
export type WorkerPoolStyle = "unified" | "split"