From 233da827686f6dd38f908519b216eacc1cc8a012 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 15 May 2026 13:35:52 +0200 Subject: [PATCH 1/2] fix(agent-manager): stabilize markdown review capture --- .changeset/markdown-review-voice.md | 5 +++++ .../unit/markdown-annotation-layer.test.ts | 15 +++++++++++++++ .../agent-manager/MarkdownAnnotationLayer.tsx | 17 +++++++++++++---- 3 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 .changeset/markdown-review-voice.md create mode 100644 packages/kilo-vscode/tests/unit/markdown-annotation-layer.test.ts diff --git a/.changeset/markdown-review-voice.md b/.changeset/markdown-review-voice.md new file mode 100644 index 00000000000..d883a488ca4 --- /dev/null +++ b/.changeset/markdown-review-voice.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Keep inline review voice capture stable in rendered Markdown diffs. diff --git a/packages/kilo-vscode/tests/unit/markdown-annotation-layer.test.ts b/packages/kilo-vscode/tests/unit/markdown-annotation-layer.test.ts new file mode 100644 index 00000000000..82865d18c43 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/markdown-annotation-layer.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "bun:test" +import { readFileSync } from "node:fs" +import { join } from "node:path" + +const file = join(import.meta.dir, "../../webview-ui/agent-manager/MarkdownAnnotationLayer.tsx") +const src = readFileSync(file, "utf8") + +describe("MarkdownAnnotationLayer", () => { + it("ignores inline annotation DOM mutations while watching rendered Markdown", () => { + expect(src).toMatch(/function isAnnotationMutation[\s\S]*target\.closest\(selector\)/) + expect(src).toMatch( + /new MutationObserver\(\(mutations\) => \{[\s\S]*mutations\.every\(isAnnotationMutation\)[\s\S]*schedule\(\)/, + ) + }) +}) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/MarkdownAnnotationLayer.tsx b/packages/kilo-vscode/webview-ui/agent-manager/MarkdownAnnotationLayer.tsx index bdd04b2b525..707b3b37fbb 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/MarkdownAnnotationLayer.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/MarkdownAnnotationLayer.tsx @@ -88,11 +88,17 @@ function matches(annotation: DiffLineAnnotation, anchor: Anchor, return true } +const selector = ".am-markdown-inline-annotations, .am-markdown-list-annotation, .am-markdown-table-annotation" + +function isAnnotationMutation(mutation: MutationRecord): boolean { + const target = mutation.target + if (!(target instanceof Element)) return false + return target.closest(selector) !== null +} + function removeInserted(root: HTMLElement, layer: HTMLElement): void { layer.replaceChildren() - root - .querySelectorAll(".am-markdown-inline-annotations, .am-markdown-list-annotation, .am-markdown-table-annotation") - .forEach((node) => node.remove()) + root.querySelectorAll(selector).forEach((node) => node.remove()) } function insertHost(anchor: Anchor, host: HTMLElement): void { @@ -206,7 +212,10 @@ export const MarkdownAnnotationLayer: Component = const root = props.root() if (!root) return observer?.disconnect() - observer = new MutationObserver(schedule) + observer = new MutationObserver((mutations) => { + if (mutations.length > 0 && mutations.every(isAnnotationMutation)) return + schedule() + }) observer.observe(root, { childList: true, subtree: true }) }) From 1acecd70375336ce72d1aec3a7e7ec69164d6e81 Mon Sep 17 00:00:00 2001 From: marius-kilocode Date: Fri, 15 May 2026 13:46:10 +0200 Subject: [PATCH 2/2] test(agent-manager): cover markdown annotation mutations --- .../unit/markdown-annotation-layer.test.ts | 25 +++++++++++-------- .../agent-manager/MarkdownAnnotationLayer.tsx | 8 +++--- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/packages/kilo-vscode/tests/unit/markdown-annotation-layer.test.ts b/packages/kilo-vscode/tests/unit/markdown-annotation-layer.test.ts index 82865d18c43..5939986abf9 100644 --- a/packages/kilo-vscode/tests/unit/markdown-annotation-layer.test.ts +++ b/packages/kilo-vscode/tests/unit/markdown-annotation-layer.test.ts @@ -1,15 +1,20 @@ import { describe, expect, it } from "bun:test" -import { readFileSync } from "node:fs" -import { join } from "node:path" +import { isAnnotationMutation } from "../../webview-ui/agent-manager/MarkdownAnnotationLayer" -const file = join(import.meta.dir, "../../webview-ui/agent-manager/MarkdownAnnotationLayer.tsx") -const src = readFileSync(file, "utf8") +function mutation(target: Node): Pick { + return { target } +} -describe("MarkdownAnnotationLayer", () => { - it("ignores inline annotation DOM mutations while watching rendered Markdown", () => { - expect(src).toMatch(/function isAnnotationMutation[\s\S]*target\.closest\(selector\)/) - expect(src).toMatch( - /new MutationObserver\(\(mutations\) => \{[\s\S]*mutations\.every\(isAnnotationMutation\)[\s\S]*schedule\(\)/, - ) +describe("isAnnotationMutation", () => { + it("matches nested annotation DOM mutations", () => { + const target = { closest: () => ({}) } as unknown as Node + expect(isAnnotationMutation(mutation(target))).toBe(true) + }) + + it("keeps external Markdown DOM mutations observable", () => { + const markdown = { closest: () => null } as unknown as Node + const plain = {} as Node + expect(isAnnotationMutation(mutation(markdown))).toBe(false) + expect(isAnnotationMutation(mutation(plain))).toBe(false) }) }) diff --git a/packages/kilo-vscode/webview-ui/agent-manager/MarkdownAnnotationLayer.tsx b/packages/kilo-vscode/webview-ui/agent-manager/MarkdownAnnotationLayer.tsx index 707b3b37fbb..2ed9ce2fb83 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/MarkdownAnnotationLayer.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/MarkdownAnnotationLayer.tsx @@ -90,9 +90,11 @@ function matches(annotation: DiffLineAnnotation, anchor: Anchor, const selector = ".am-markdown-inline-annotations, .am-markdown-list-annotation, .am-markdown-table-annotation" -function isAnnotationMutation(mutation: MutationRecord): boolean { +// Keep host insertion observable, only nested annotation UI updates are safe to ignore. +export function isAnnotationMutation(mutation: Pick): boolean { const target = mutation.target - if (!(target instanceof Element)) return false + if (!("closest" in target)) return false + if (typeof target.closest !== "function") return false return target.closest(selector) !== null } @@ -213,7 +215,7 @@ export const MarkdownAnnotationLayer: Component = if (!root) return observer?.disconnect() observer = new MutationObserver((mutations) => { - if (mutations.length > 0 && mutations.every(isAnnotationMutation)) return + if (mutations.every(isAnnotationMutation)) return schedule() }) observer.observe(root, { childList: true, subtree: true })