mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-11 13:33:25 +00:00
fix(session-ui): bound completed markdown rendering
This commit is contained in:
parent
ccc11dc92d
commit
9b16d0b069
3 changed files with 54 additions and 16 deletions
|
|
@ -158,6 +158,12 @@ describe("markdown stream", () => {
|
|||
expect(final.blocks[2]).toEqual({ raw: "- final item", src: "- final item", mode: "full" })
|
||||
})
|
||||
|
||||
test("splits completed markdown into bounded top-level blocks", () => {
|
||||
const result = project(undefined, "# Plan\n\nFirst paragraph.\n\nSecond paragraph.", false)
|
||||
|
||||
expect(result.blocks.map((block) => block.raw)).toEqual(["# Plan", "First paragraph.", "Second paragraph."])
|
||||
})
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ function heal(text: string) {
|
|||
}
|
||||
|
||||
export function stream(text: string, live: boolean): Block[] {
|
||||
if (!live) return completedProjection(text).blocks
|
||||
if (!live) return completedBlocks(text)
|
||||
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")
|
||||
|
|
@ -85,6 +85,17 @@ export function stream(text: string, live: boolean): Block[] {
|
|||
return [...result, { raw, src: openCode(code.raw), mode: "code", language: language(code.lang) }]
|
||||
}
|
||||
|
||||
function completedBlocks(text: string) {
|
||||
if (refs(text)) return completedProjection(text).blocks
|
||||
const tokens = marked.lexer(text)
|
||||
return tokens.flatMap((token): Block[] => {
|
||||
if (token.type === "space") return []
|
||||
if (token.type !== "code") return [{ raw: token.raw, src: token.raw, mode: "full" }]
|
||||
const code = token as Tokens.Code
|
||||
return [{ raw: code.raw, src: code.text, mode: "code", language: language(code.lang), complete: true }]
|
||||
})
|
||||
}
|
||||
|
||||
export function project(previous: Projection | undefined, text: string, live: boolean): Projection {
|
||||
if (!live) {
|
||||
const current =
|
||||
|
|
@ -93,7 +104,7 @@ export function project(previous: Projection | undefined, text: string, live: bo
|
|||
: previous && text.startsWith(previous.text)
|
||||
? project(previous, text, true)
|
||||
: undefined
|
||||
if (!current) return completedProjection(text)
|
||||
if (!current) return { text, blocks: completedBlocks(text) }
|
||||
return {
|
||||
text,
|
||||
blocks: current.blocks.map((block) => {
|
||||
|
|
|
|||
|
|
@ -491,6 +491,8 @@ export function Markdown(
|
|||
)
|
||||
|
||||
let copyCleanup: (() => void) | undefined
|
||||
let renderFrame: number | undefined
|
||||
let renderGeneration = 0
|
||||
|
||||
createEffect(() => {
|
||||
const container = root()
|
||||
|
|
@ -499,6 +501,9 @@ export function Markdown(
|
|||
const content = local.text ? pendingBlocks(result, projected, local.cacheKey, owner) : []
|
||||
if (!container) return
|
||||
if (isServer) return
|
||||
const generation = ++renderGeneration
|
||||
if (renderFrame !== undefined) cancelAnimationFrame(renderFrame)
|
||||
renderFrame = undefined
|
||||
if (content.length === 0) {
|
||||
disposeCopyButtons(container)
|
||||
container.innerHTML = ""
|
||||
|
|
@ -515,24 +520,40 @@ export function Markdown(
|
|||
})
|
||||
activeCodeKeys.clear()
|
||||
nextCodeKeys.forEach((key) => activeCodeKeys.add(key))
|
||||
content.forEach((block, index) => updateBlock(container, index, block, labels))
|
||||
while (container.children.length > content.length) {
|
||||
const child = container.lastElementChild
|
||||
if (!child) break
|
||||
disposeCopyButtons(child)
|
||||
child.remove()
|
||||
let index = 0
|
||||
const update = () => {
|
||||
renderFrame = undefined
|
||||
if (generation !== renderGeneration) return
|
||||
const deadline = performance.now() + 8
|
||||
while (index < content.length && performance.now() < deadline) {
|
||||
updateBlock(container, index, content[index]!, labels)
|
||||
index += 1
|
||||
}
|
||||
if (index < content.length) {
|
||||
renderFrame = requestAnimationFrame(update)
|
||||
return
|
||||
}
|
||||
while (container.children.length > content.length) {
|
||||
const child = container.lastElementChild
|
||||
if (!child) break
|
||||
disposeCopyButtons(child)
|
||||
child.remove()
|
||||
}
|
||||
container
|
||||
.querySelectorAll<HTMLElement>('[data-slot="markdown-copy-button"]')
|
||||
.forEach((button) => setCopyState(button, labels, button.dataset.copied === "true"))
|
||||
if (!copyCleanup)
|
||||
copyCleanup = setupCodeCopy(container, () => ({
|
||||
copy: i18n.t("ui.message.copy"),
|
||||
copied: i18n.t("ui.message.copied"),
|
||||
}))
|
||||
}
|
||||
container
|
||||
.querySelectorAll<HTMLElement>('[data-slot="markdown-copy-button"]')
|
||||
.forEach((button) => setCopyState(button, labels, button.dataset.copied === "true"))
|
||||
if (!copyCleanup)
|
||||
copyCleanup = setupCodeCopy(container, () => ({
|
||||
copy: i18n.t("ui.message.copy"),
|
||||
copied: i18n.t("ui.message.copied"),
|
||||
}))
|
||||
update()
|
||||
})
|
||||
|
||||
onCleanup(() => {
|
||||
renderGeneration += 1
|
||||
if (renderFrame !== undefined) cancelAnimationFrame(renderFrame)
|
||||
if (copyCleanup) copyCleanup()
|
||||
disposeMarkdownProjection(owner)
|
||||
activeCodeKeys.forEach(disposeCode)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue