feat: gate every surface on CoeditingRegistry (INV-10) — commenting ranges re-assigned on gate change (spec §6.4 v0.2.1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-07-02 07:41:13 -07:00
parent cf65528711
commit a323b827a8
18 changed files with 171 additions and 18 deletions
+15
View File
@@ -17,6 +17,7 @@ import type { DiffViewController } from "./diffViewController";
import type { AttributionController } from "./attributionController";
import { decorationPlan, wordEditHunks, authorAt } from "./trackChangesModel";
import { isAuthorable } from "./workspacePath";
import type { CoeditingRegistry } from "./coeditingRegistry";
export class EditorProposalController implements vscode.Disposable, vscode.CodeLensProvider {
private readonly disposables: vscode.Disposable[] = [];
@@ -51,6 +52,7 @@ export class EditorProposalController implements vscode.Disposable, vscode.CodeL
private readonly proposals: ProposalController,
private readonly diffView: DiffViewController,
private readonly attribution: AttributionController,
private readonly registry: CoeditingRegistry,
) {
this.disposables.push(
this.insDeco.human, this.insDeco.claude, this.delDeco.human, this.delDeco.claude, this.delDeco.none, this.lensEmitter,
@@ -67,6 +69,13 @@ export class EditorProposalController implements vscode.Disposable, vscode.CodeL
const ed = vscode.window.activeTextEditor;
if (ed && e.document === ed.document) this.scheduleRender(ed);
}),
// INV-10: re-render (and refresh CodeLens) on every gate change — exit clears
// decorations/CodeLens via renderEditor's own gate check, re-entry redraws.
this.registry.onDidChange(({ uri }) => {
const ed = vscode.window.visibleTextEditors.find((e) => e.document.uri.toString() === uri);
if (ed) this.renderEditor(ed);
this.lensEmitter.fire();
}),
// the four QuickPick-backed menu commands
vscode.commands.registerCommand("cowriting.proposalAcceptMenu", (id?: string) => this.menu("accept", id)),
vscode.commands.registerCommand("cowriting.proposalRejectMenu", (id?: string) => this.menu("reject", id)),
@@ -106,6 +115,9 @@ export class EditorProposalController implements vscode.Disposable, vscode.CodeL
private async onProposalsChanged(uri: string): Promise<void> {
const doc = vscode.workspace.textDocuments.find((d) => d.uri.toString() === uri);
if (!doc || doc.languageId !== "markdown" || !isAuthorable(doc.uri.scheme)) return;
// INV-10: a doc that exited coediting between the schedule and this tick gets
// no optimistic-apply/decorations/CodeLens.
if (!this.registry.isCoediting(doc.uri)) return;
const key = this.proposals.keyFor(doc);
for (const v of this.proposals.listProposals(doc)) {
if (v.anchorStart !== null && !this.proposals.isApplied(key, v.id)) {
@@ -125,6 +137,8 @@ export class EditorProposalController implements vscode.Disposable, vscode.CodeL
for (const a of ["human", "claude", "none"] as const) editor.setDecorations(this.delDeco[a], []);
};
if (doc.languageId !== "markdown") return clearAll();
// INV-10: a non-entered doc gets no decorations at all.
if (!this.registry.isCoediting(doc.uri)) return clearAll();
const key = this.proposals.keyFor(doc);
const ins: Record<"human" | "claude", vscode.Range[]> = { human: [], claude: [] };
const del: Record<"human" | "claude" | "none", vscode.DecorationOptions[]> = { human: [], claude: [], none: [] };
@@ -205,6 +219,7 @@ export class EditorProposalController implements vscode.Disposable, vscode.CodeL
/** CodeLensProvider: a `Accept ▾` / `Reject ▾` pair above each applied block. */
provideCodeLenses(document: vscode.TextDocument): vscode.CodeLens[] {
if (document.languageId !== "markdown") return [];
if (!this.registry.isCoediting(document.uri)) return [];
const key = this.proposals.keyFor(document);
const lenses: vscode.CodeLens[] = [];
for (const v of this.proposals.listProposals(document)) {