feat: authorship + change annotations in the built-in Markdown preview (D3/D21, PUC-3); cowriting.annotations toggle

Task 7 of the native-surfaces migration plan. Pure previewAnnotations.ts
(annotateSource + cowritingMarkdownItPlugin) reuses trackChangesModel.ts's
sentinel discipline (#33/#47), generalized to a 3-way ins-claude/ins-human/del
tag (exported injectSentinels/sentinelsToSpans, colorByAuthor unaffected,
84/84 existing tests pass unmodified). Host hook in extension.ts wires
registry/diffView/attribution/proposals/config into the plugin, exposed via
CowritingApi.extendMarkdownIt + a previewAnnotationHost test seam.
cowriting.annotations setting + toggleAnnotations command/menu. Q4 mermaid
(previewScripts + options.highlight fence override) implemented per the
proven bierner.markdown-mermaid pattern; intra-diagram diff augmentation
scoped out (unverifiable via the structural E2E harness) — see
task-7-report.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-07-02 14:23:11 -07:00
parent c9975ba9e6
commit 17fc01e8d8
9 changed files with 576 additions and 29 deletions
+79
View File
@@ -0,0 +1,79 @@
import * as assert from "node:assert";
import MarkdownIt from "markdown-it";
import * as vscode from "vscode";
import { cowritingMarkdownItPlugin } from "../../../src/previewAnnotations";
import { activateApi, settle } from "./helpers";
// Task 7 (D3/D21, PUC-3): the built-in preview's DOM isn't queryable from a
// host E2E test (§6.8), so this asserts the pure transform's INPUTS/OUTPUTS
// through the real `CowritingApi` seams instead — the production
// `previewAnnotationHost.inputsFor` (not a hand-rolled reimplementation) fed
// through the real `cowritingMarkdownItPlugin` + a fresh markdown-it instance
// (mirroring the unit suite's own `render()` helper), so this exercises the
// ACTUAL registry/diffView/attribution/proposals/config wiring end to end.
suite("PUC-3 annotate-toggle (built-in Markdown preview annotations)", () => {
test("a machine-attributed change renders cw-ins-claude; disabling the setting passes the source through unchanged", async () => {
const api = await activateApi();
const doc = await vscode.workspace.openTextDocument({
language: "markdown",
content: "hello world\n",
});
await vscode.window.showTextDocument(doc);
await vscode.commands.executeCommand("cowriting.coeditDocument");
await settle();
const start = doc.getText().indexOf("world");
const ok = await vscode.commands.executeCommand<boolean>("cowriting.applyAgentEdit", {
uri: doc.uri.toString(),
start,
end: start + "world".length,
newText: "brave new world",
model: "sonnet",
sessionId: "e2e-preview-annotations",
turnId: "turn-e2e-preview-annotations",
});
assert.strictEqual(ok, true, "seam edit applies");
await settle();
// Annotations ON (default): render the CURRENT buffer through the real
// markdown-it plugin + the production host — proves the full pipeline
// (core-rule swap → full-render sentinel walk) reaches a live doc.
const md = new MarkdownIt();
cowritingMarkdownItPlugin(md, api.previewAnnotationHost);
const html = md.render(doc.getText());
assert.ok(html.includes("cw-ins-claude"), `expected cw-ins-claude in:\n${html}`);
// Toggle OFF: the config-gated `enabled` flag reaches `annotateSource` and
// the transform becomes the identity (no core-rule swap, no marks).
const config = vscode.workspace.getConfiguration("cowriting");
await config.update("annotations", false, vscode.ConfigurationTarget.Global);
try {
const inputs = api.previewAnnotationHost.inputsFor(undefined);
assert.ok(inputs, "inputsFor still resolves a coedited doc while disabled");
assert.strictEqual(inputs!.enabled, false);
const md2 = new MarkdownIt();
cowritingMarkdownItPlugin(md2, api.previewAnnotationHost);
const plainMd = new MarkdownIt();
assert.strictEqual(md2.render(doc.getText()), plainMd.render(doc.getText()));
} finally {
await config.update("annotations", true, vscode.ConfigurationTarget.Global);
}
});
test("cowriting.toggleAnnotations flips the setting", async () => {
const api = await activateApi();
const doc = await vscode.workspace.openTextDocument({ language: "markdown", content: "toggle me\n" });
await vscode.window.showTextDocument(doc);
await vscode.commands.executeCommand("cowriting.coeditDocument");
await settle();
const config = () => vscode.workspace.getConfiguration("cowriting");
const before = config().get<boolean>("annotations", true);
await vscode.commands.executeCommand("cowriting.toggleAnnotations");
assert.strictEqual(config().get<boolean>("annotations", true), !before);
// restore
await vscode.commands.executeCommand("cowriting.toggleAnnotations");
assert.strictEqual(config().get<boolean>("annotations", true), before);
void api; // keep the activated extension alive for the duration of the test
});
});
+46
View File
@@ -0,0 +1,46 @@
import MarkdownIt from "markdown-it";
import { describe, expect, it } from "vitest";
import { cowritingMarkdownItPlugin } from "../src/previewAnnotations";
function render(src: string, inputs: any): string {
const md = new MarkdownIt();
cowritingMarkdownItPlugin(md, { inputsFor: () => inputs });
return md.render(src);
}
const base = { baselineReason: "entered", proposals: [], enabled: true };
describe("preview annotations (PUC-3/D21)", () => {
it("renders clean when disabled", () => {
const html = render("hello brave world\n", { ...base, enabled: false, baselineText: "hello world\n", spans: [] });
expect(html).not.toContain("cw-");
});
it("colors machine spans blue and human insertions green vs baseline", () => {
const html = render("hello brave new world\n", {
...base,
baselineText: "hello world\n",
spans: [{ start: 6, end: 12, author: "claude" }, { start: 12, end: 16, author: "human" }],
});
expect(html).toContain('class="cw-ins-claude"');
expect(html).toContain('class="cw-ins-human"');
});
it("strikes deletions vs baseline", () => {
const html = render("hello world\n", { ...base, baselineText: "hello cruel world\n", spans: [] });
expect(html).toContain("cw-del");
expect(html).toContain("cruel");
});
it("pinned baseline renders clean even with spans (pin→clean, INV-33/#48)", () => {
const html = render("hello brave world\n", {
...base, baselineReason: "pinned", baselineText: "hello brave world\n",
spans: [{ start: 6, end: 12, author: "claude" }],
});
expect(html).not.toContain("cw-ins");
});
it("survives intra-emphasis boundaries (#33 discipline)", () => {
const html = render("a *bold claim* here\n", {
...base, baselineText: "a here\n", spans: [{ start: 2, end: 14, author: "claude" }],
});
expect(html).toContain("cw-ins-claude");
expect(html).not.toMatch(/[-]/); // no PUA sentinel leaks
});
});