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..5939986abf9 --- /dev/null +++ b/packages/kilo-vscode/tests/unit/markdown-annotation-layer.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from "bun:test" +import { isAnnotationMutation } from "../../webview-ui/agent-manager/MarkdownAnnotationLayer" + +function mutation(target: Node): Pick { + return { target } +} + +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 bdd04b2b525..2ed9ce2fb83 100644 --- a/packages/kilo-vscode/webview-ui/agent-manager/MarkdownAnnotationLayer.tsx +++ b/packages/kilo-vscode/webview-ui/agent-manager/MarkdownAnnotationLayer.tsx @@ -88,11 +88,19 @@ function matches(annotation: DiffLineAnnotation, anchor: Anchor, return true } +const selector = ".am-markdown-inline-annotations, .am-markdown-list-annotation, .am-markdown-table-annotation" + +// Keep host insertion observable, only nested annotation UI updates are safe to ignore. +export function isAnnotationMutation(mutation: Pick): boolean { + const target = mutation.target + if (!("closest" in target)) return false + if (typeof target.closest !== "function") 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 +214,10 @@ export const MarkdownAnnotationLayer: Component = const root = props.root() if (!root) return observer?.disconnect() - observer = new MutationObserver(schedule) + observer = new MutationObserver((mutations) => { + if (mutations.every(isAnnotationMutation)) return + schedule() + }) observer.observe(root, { childList: true, subtree: true }) })