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:
@@ -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
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user