From 0ec80b0c6f401253fc05a7bd5c876e94320dfaa1 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 21 May 2026 14:27:34 +0200 Subject: [PATCH 1/3] fix: stabilize streamed markdown code blocks --- .changeset/steady-code-streams.md | 7 ++ .../kilo-ui/src/components/message-part.tsx | 9 +- .../tests/unit/kilo-ui-contract.test.ts | 11 +++ .../tests/unit/markdown-raf-coalesce.test.ts | 8 ++ packages/ui/src/components/markdown.tsx | 2 + .../src/kilocode/markdown-stream-highlight.ts | 94 +++++++++++++++++++ 6 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 .changeset/steady-code-streams.md create mode 100644 packages/ui/src/kilocode/markdown-stream-highlight.ts diff --git a/.changeset/steady-code-streams.md b/.changeset/steady-code-streams.md new file mode 100644 index 00000000000..3a2dbf43471 --- /dev/null +++ b/.changeset/steady-code-streams.md @@ -0,0 +1,7 @@ +--- +"kilo-code": patch +"@kilocode/kilo-ui": patch +"@opencode-ai/ui": patch +--- + +Keep streamed Markdown code blocks stable while assistant output is still arriving. diff --git a/packages/kilo-ui/src/components/message-part.tsx b/packages/kilo-ui/src/components/message-part.tsx index 6e7905c478a..b05458622a1 100644 --- a/packages/kilo-ui/src/components/message-part.tsx +++ b/packages/kilo-ui/src/components/message-part.tsx @@ -1377,7 +1377,12 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
- +
@@ -1575,7 +1580,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props: MessagePartProp
- +
diff --git a/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts b/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts index 784aa019e40..7f7ca51ae43 100644 --- a/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts @@ -137,6 +137,17 @@ describe("DataProvider contract (runtime)", () => { }) }) +describe("Assistant Markdown streaming contract (source)", () => { + const src = fs.readFileSync(KILO_MESSAGE_PART_FILE, "utf-8") + const block = + src.match(/PART_MAPPING\["text"\]\s*=\s*function TextPartDisplay[\s\S]*?(?=\/\/ Expanded mode|PART_MAPPING\["reasoning"\])/)?.[0] ?? "" + + it("passes active text streams through Markdown's streaming mode", () => { + expect(block).not.toBe("") + expect(block).toContain("streaming={streaming()}") + }) +}) + describe("Edit tool diff-first click contract (source)", () => { const src = fs.readFileSync(KILO_MESSAGE_PART_FILE, "utf-8") diff --git a/packages/kilo-vscode/tests/unit/markdown-raf-coalesce.test.ts b/packages/kilo-vscode/tests/unit/markdown-raf-coalesce.test.ts index a584092361c..b67f4b9e750 100644 --- a/packages/kilo-vscode/tests/unit/markdown-raf-coalesce.test.ts +++ b/packages/kilo-vscode/tests/unit/markdown-raf-coalesce.test.ts @@ -24,6 +24,7 @@ import { join } from "node:path" */ describe("Markdown rAF-coalesced parse — regression guard", () => { const path = join(__dirname, "..", "..", "..", "ui", "src", "components", "markdown.tsx") + const helper = join(__dirname, "..", "..", "..", "ui", "src", "kilocode", "markdown-stream-highlight.ts") const stripComments = (src: string): string => src @@ -32,6 +33,7 @@ describe("Markdown rAF-coalesced parse — regression guard", () => { .replace(/^\s*\/\/.*$/gm, "") const src = stripComments(readFileSync(path, "utf8")) + const body = stripComments(readFileSync(helper, "utf8")) it("render effect uses requestAnimationFrame to coalesce parses", () => { // Locate the createEffect that owns the morphdom call. @@ -50,4 +52,10 @@ describe("Markdown rAF-coalesced parse — regression guard", () => { // rapid updates can collapse into it. expect(src).toMatch(/\b(pendingFrame|pendingContent)\b/) }) + + it("delegates streamed Shiki refreshes to the Kilo-owned helper", () => { + expect(src).toContain("preserveStreamingHighlight(fromEl, toEl, local.streaming ?? false)") + expect(body).toContain("export function preserveStreamingHighlight") + expect(body).toMatch(/continues\(before, after\)[\s\S]*queue\(from, after, lang\)/) + }) }) diff --git a/packages/ui/src/components/markdown.tsx b/packages/ui/src/components/markdown.tsx index bde83b24b58..a29fd870a6b 100644 --- a/packages/ui/src/components/markdown.tsx +++ b/packages/ui/src/components/markdown.tsx @@ -8,6 +8,7 @@ import { isServer } from "solid-js/web" import { stream } from "./markdown-stream" import { tryFastRender } from "../kilocode/markdown-fast-path" // kilocode_change import { hasMermaid, preserveMermaid, renderMermaid, type MermaidLabels } from "../kilocode/markdown-mermaid" // kilocode_change +import { preserveStreamingHighlight } from "../kilocode/markdown-stream-highlight" // kilocode_change type Entry = { hash: string @@ -434,6 +435,7 @@ export function Markdown( const fromHash = fromEl.getAttribute("data-source-hash") const toCode = toEl.querySelector("code")?.textContent ?? "" if (fromHash === fnv1a(toCode)) return false + if (preserveStreamingHighlight(fromEl, toEl, local.streaming ?? false)) return false // kilocode_change // Source changed during streaming — fall through so morphdom replaces // kilocode_change // the stale highlighted block with the updated plain block, which will // be re-highlighted on the next deferredHighlight pass. diff --git a/packages/ui/src/kilocode/markdown-stream-highlight.ts b/packages/ui/src/kilocode/markdown-stream-highlight.ts new file mode 100644 index 00000000000..cf314d2c7af --- /dev/null +++ b/packages/ui/src/kilocode/markdown-stream-highlight.ts @@ -0,0 +1,94 @@ +import { getSharedHighlighter } from "@pierre/diffs" +import { bundledLanguages, type BundledLanguage } from "shiki" +import { fnv1a } from "../context/marked" + +type Job = { + code: string + lang: string + busy: boolean +} + +const jobs = new WeakMap() + +function continues(before: string, after: string) { + const base = before.endsWith("\n") ? before.slice(0, -1) : before + return !!base && after.startsWith(base) +} + +async function source(lang: string, code: string) { + try { + const highlighter = await getSharedHighlighter({ themes: ["Kilo"], langs: [] }) + const language = lang in bundledLanguages ? lang : "text" + if (!highlighter.getLoadedLanguages().includes(language)) { + await highlighter.loadLanguage(language as BundledLanguage) + } + return highlighter.codeToHtml(code, { lang: language, theme: "Kilo", tabindex: false }) + } catch (err) { + console.warn("Streaming code highlight failed", lang, err) + return + } +} + +function update(pre: HTMLPreElement, html: string, code: string) { + if (!pre.isConnected) return + const temp = document.createElement("div") + temp.innerHTML = html + const next = temp.firstElementChild + if (!(next instanceof HTMLPreElement)) return + const x = pre.scrollLeft + for (const name of pre.getAttributeNames()) { + pre.removeAttribute(name) + } + for (const attr of next.attributes) { + pre.setAttribute(attr.name, attr.value) + } + pre.setAttribute("data-source-hash", fnv1a(code)) + pre.replaceChildren(...Array.from(next.childNodes)) + pre.scrollLeft = x +} + +async function refresh(pre: HTMLPreElement, code: string, lang: string) { + if (!pre.isConnected || !code) return + await new Promise((resolve) => setTimeout(resolve, 0)) + if (!pre.isConnected) return + const html = await source(lang, code) + if (!html || !pre.isConnected) return + update(pre, html, code) +} + +function run(pre: HTMLPreElement, job: Job) { + const code = job.code + const lang = job.lang + job.busy = true + const done = () => { + if (!pre.isConnected) return + if (job.code !== code || job.lang !== lang) { + job.busy = false + run(pre, job) + return + } + job.busy = false + } + void refresh(pre, code, lang).then(done, done) +} + +function queue(pre: HTMLPreElement, code: string, lang: string) { + const job = jobs.get(pre) ?? { code, lang, busy: false } + job.code = code + job.lang = lang + jobs.set(pre, job) + if (job.busy) return + run(pre, job) +} + +export function preserveStreamingHighlight(from: Element, to: Element, streaming: boolean) { + if (!streaming) return false + if (!(from instanceof HTMLPreElement) || !(to instanceof HTMLPreElement)) return false + if (!from.classList.contains("shiki") || to.classList.contains("shiki")) return false + const before = from.querySelector("code")?.textContent ?? "" + const after = to.querySelector("code")?.textContent ?? "" + const lang = to.querySelector("code")?.getAttribute("data-lang") || "text" + if (!after || lang === "mermaid" || !continues(before, after)) return false + queue(from, after, lang) + return true +} From 2d6fa739932ae894e0104e61d839d8a620c558c1 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Thu, 21 May 2026 14:31:23 +0200 Subject: [PATCH 2/3] style(vscode): format streaming markdown regression --- packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts b/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts index 7f7ca51ae43..4d1c17f2f6e 100644 --- a/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts +++ b/packages/kilo-vscode/tests/unit/kilo-ui-contract.test.ts @@ -140,7 +140,9 @@ describe("DataProvider contract (runtime)", () => { describe("Assistant Markdown streaming contract (source)", () => { const src = fs.readFileSync(KILO_MESSAGE_PART_FILE, "utf-8") const block = - src.match(/PART_MAPPING\["text"\]\s*=\s*function TextPartDisplay[\s\S]*?(?=\/\/ Expanded mode|PART_MAPPING\["reasoning"\])/)?.[0] ?? "" + src.match( + /PART_MAPPING\["text"\]\s*=\s*function TextPartDisplay[\s\S]*?(?=\/\/ Expanded mode|PART_MAPPING\["reasoning"\])/, + )?.[0] ?? "" it("passes active text streams through Markdown's streaming mode", () => { expect(block).not.toBe("") From 24d8311ab45806795daa7883e2cc15b2121548e1 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Tue, 26 May 2026 10:33:58 +0200 Subject: [PATCH 3/3] fix(ui): release interrupted markdown highlight jobs --- packages/kilo-vscode/tests/unit/markdown-raf-coalesce.test.ts | 1 + packages/ui/src/kilocode/markdown-stream-highlight.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/markdown-raf-coalesce.test.ts b/packages/kilo-vscode/tests/unit/markdown-raf-coalesce.test.ts index b67f4b9e750..8a582935abc 100644 --- a/packages/kilo-vscode/tests/unit/markdown-raf-coalesce.test.ts +++ b/packages/kilo-vscode/tests/unit/markdown-raf-coalesce.test.ts @@ -57,5 +57,6 @@ describe("Markdown rAF-coalesced parse — regression guard", () => { expect(src).toContain("preserveStreamingHighlight(fromEl, toEl, local.streaming ?? false)") expect(body).toContain("export function preserveStreamingHighlight") expect(body).toMatch(/continues\(before, after\)[\s\S]*queue\(from, after, lang\)/) + expect(body).toMatch(/const done = \(\) => \{\s*job\.busy = false\s*if \(!pre\.isConnected\) return/) }) }) diff --git a/packages/ui/src/kilocode/markdown-stream-highlight.ts b/packages/ui/src/kilocode/markdown-stream-highlight.ts index cf314d2c7af..993199a0933 100644 --- a/packages/ui/src/kilocode/markdown-stream-highlight.ts +++ b/packages/ui/src/kilocode/markdown-stream-highlight.ts @@ -61,13 +61,12 @@ function run(pre: HTMLPreElement, job: Job) { const lang = job.lang job.busy = true const done = () => { + job.busy = false if (!pre.isConnected) return if (job.code !== code || job.lang !== lang) { - job.busy = false run(pre, job) return } - job.busy = false } void refresh(pre, code, lang).then(done, done) }