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
+61
View File
@@ -18,6 +18,8 @@ import { EditorProposalController } from "./editorProposalController";
import { isAuthorable, routeEdit } from "./workspacePath";
import { CoeditingRegistry } from "./coeditingRegistry";
import { ScmSurfaceController } from "./scmSurface";
import type MarkdownIt from "markdown-it";
import { cowritingMarkdownItPlugin, type AnnotationInputs } from "./previewAnnotations";
const CHANNEL_NAME = "Cowriting (Cline SDK)";
@@ -34,6 +36,13 @@ export interface CowritingApi {
editorProposalController: EditorProposalController;
coeditingRegistry: CoeditingRegistry;
scmSurfaceController: ScmSurfaceController;
/** Task 7 test seam: the REAL production `inputsFor`, so E2E exercises the
* actual registry/diffView/attribution/proposals/config wiring (not a
* hand-rolled reimplementation of it). */
previewAnnotationHost: { inputsFor(env: unknown): AnnotationInputs | undefined };
/** Task 7 (D3/D21): the markdown-it plugin factory VS Code's built-in preview
* loads via `markdown.markdownItPlugins` (package.json). */
extendMarkdownIt: (md: unknown) => unknown;
}
export function activate(context: vscode.ExtensionContext): CowritingApi | undefined {
@@ -391,6 +400,56 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
vscode.workspace.textDocuments.forEach(renderIfOpen);
context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(renderIfOpen));
// --- Task 7 (D3/D21, PUC-3): annotations in the BUILT-IN Markdown preview ---
// `env.currentDocument` (VS Code's markdown-it render env) is an OBSERVED
// preview-engine behavior, not a documented contract, so `lastActiveCoedited`
// is the fallback that keeps single-doc correctness when it's absent — the
// last editor that WAS coediting when it lost focus (e.g. focus moved to the
// preview pane itself) stays the annotation target until a different
// coedited doc becomes active.
let lastCoeditedUri: string | undefined;
const trackLastCoedited = (ed: vscode.TextEditor | undefined) => {
if (ed && coeditingRegistry.isCoediting(ed.document.uri)) lastCoeditedUri = ed.document.uri.toString();
};
trackLastCoedited(vscode.window.activeTextEditor);
context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(trackLastCoedited),
// The active editor doesn't itself change when `cowriting.coeditDocument`
// enters IT into coediting — re-check on every gate change too, so the
// very first render after entering already has an annotation target.
coeditingRegistry.onDidChange(() => trackLastCoedited(vscode.window.activeTextEditor)),
);
const previewAnnotationHost = {
inputsFor(env: unknown): AnnotationInputs | undefined {
const envUri = (env as { currentDocument?: { toString(): string } } | undefined)?.currentDocument?.toString();
const key = envUri && coeditingRegistry.list().includes(envUri) ? envUri : lastCoeditedUri;
if (!key) return undefined;
const doc = vscode.workspace.textDocuments.find((d) => d.uri.toString() === key);
if (!doc) return undefined;
const baseline = diffViewController.getBaseline(key);
return {
baselineText: baseline?.text,
baselineReason: baseline?.reason,
spans: attributionController.spansFor(doc),
proposals: proposalController.listProposals(doc),
enabled: vscode.workspace.getConfiguration("cowriting").get<boolean>("annotations", true),
};
},
};
// Toggle: flips `cowriting.annotations` + refreshes the built-in preview
// (VS Code re-invokes extendMarkdownIt's plugin on the next render — no
// extension-side re-render call exists beyond the standard refresh command).
context.subscriptions.push(
vscode.commands.registerCommand("cowriting.toggleAnnotations", async () => {
const config = vscode.workspace.getConfiguration("cowriting");
const current = config.get<boolean>("annotations", true);
await config.update("annotations", !current, vscode.ConfigurationTarget.Global);
await vscode.commands.executeCommand("markdown.preview.refresh");
}),
);
return {
threadController,
attributionController,
@@ -404,6 +463,8 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
editorProposalController,
coeditingRegistry,
scmSurfaceController,
previewAnnotationHost,
extendMarkdownIt: (md: unknown) => cowritingMarkdownItPlugin(md as MarkdownIt, previewAnnotationHost),
};
}