feat: CoeditingRegistry opt-in gate (INV-10, spec §6.4) — enter/stop commands + context keys
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* CoeditingRegistry — the INV-10 activation gate (spec §6.4). Holds the set of
|
||||
* documents the writer has explicitly entered into coediting; every surface
|
||||
* (SCM/QuickDiff, comments, preview annotations, edit commands, decorations)
|
||||
* checks membership before attaching. Persisted in workspaceState so a reload
|
||||
* restores the set (the durable sidecar carries the artifacts; this carries
|
||||
* only the mode).
|
||||
*/
|
||||
import * as vscode from "vscode";
|
||||
|
||||
const STATE_KEY = "cowriting.coeditingSet";
|
||||
|
||||
export class CoeditingRegistry implements vscode.Disposable {
|
||||
private readonly set: Set<string>;
|
||||
private readonly emitter = new vscode.EventEmitter<{ uri: string; coediting: boolean }>();
|
||||
readonly onDidChange = this.emitter.event;
|
||||
/** Set by the SCM surface (Task 3) so syncContext can expose the baseline mode. */
|
||||
baselineModeOf: ((uri: vscode.Uri) => "head" | "snapshot" | undefined) | undefined;
|
||||
|
||||
constructor(private readonly state: vscode.Memento) {
|
||||
this.set = new Set(state.get<string[]>(STATE_KEY, []));
|
||||
}
|
||||
|
||||
enter(uri: vscode.Uri): void {
|
||||
const key = uri.toString();
|
||||
if (this.set.has(key)) return;
|
||||
this.set.add(key);
|
||||
void this.state.update(STATE_KEY, [...this.set]);
|
||||
this.emitter.fire({ uri: key, coediting: true });
|
||||
}
|
||||
|
||||
exit(uri: vscode.Uri): void {
|
||||
const key = uri.toString();
|
||||
if (!this.set.delete(key)) return;
|
||||
void this.state.update(STATE_KEY, [...this.set]);
|
||||
this.emitter.fire({ uri: key, coediting: false });
|
||||
}
|
||||
|
||||
isCoediting(uri: vscode.Uri): boolean {
|
||||
return this.set.has(uri.toString());
|
||||
}
|
||||
|
||||
list(): string[] {
|
||||
return [...this.set];
|
||||
}
|
||||
|
||||
syncContext(editor: vscode.TextEditor | undefined): void {
|
||||
const coediting = !!editor && this.isCoediting(editor.document.uri);
|
||||
void vscode.commands.executeCommand("setContext", "cowriting.isCoediting", coediting);
|
||||
const mode = coediting && editor ? this.baselineModeOf?.(editor.document.uri) : undefined;
|
||||
void vscode.commands.executeCommand("setContext", "cowriting.baselineMode", mode ?? "");
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.emitter.dispose();
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { TrackChangesPreviewController } from "./trackChangesPreview";
|
||||
import { LiveProgressUi } from "./liveProgressUi";
|
||||
import { EditorProposalController } from "./editorProposalController";
|
||||
import { isAuthorable, routeEdit, selectionRejection } from "./workspacePath";
|
||||
import { CoeditingRegistry } from "./coeditingRegistry";
|
||||
|
||||
const CHANNEL_NAME = "Cowriting (Cline SDK)";
|
||||
|
||||
@@ -27,6 +28,7 @@ export interface CowritingApi {
|
||||
sidecarRouter: SidecarRouter;
|
||||
liveProgressUi: LiveProgressUi;
|
||||
editorProposalController: EditorProposalController;
|
||||
coeditingRegistry: CoeditingRegistry;
|
||||
}
|
||||
|
||||
export function activate(context: vscode.ExtensionContext): CowritingApi | undefined {
|
||||
@@ -59,6 +61,32 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
||||
}),
|
||||
);
|
||||
|
||||
// --- PUC-7 (native-surfaces migration): the INV-10 opt-in gate ---
|
||||
// Every later native surface (SCM/QuickDiff, comments, preview annotations,
|
||||
// edit commands, decorations) checks isCoediting before attaching. Persisted
|
||||
// in workspaceState so a reload restores the set.
|
||||
const coeditingRegistry = new CoeditingRegistry(context.workspaceState);
|
||||
context.subscriptions.push(coeditingRegistry);
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cowriting.coeditDocument", () => {
|
||||
const ed = vscode.window.activeTextEditor;
|
||||
if (!ed || ed.document.languageId !== "markdown" || !isAuthorable(ed.document.uri.scheme)) {
|
||||
void vscode.window.showWarningMessage("Cowriting: open a Markdown document to coedit it.");
|
||||
return;
|
||||
}
|
||||
coeditingRegistry.enter(ed.document.uri);
|
||||
coeditingRegistry.syncContext(ed);
|
||||
}),
|
||||
vscode.commands.registerCommand("cowriting.stopCoediting", () => {
|
||||
const ed = vscode.window.activeTextEditor;
|
||||
if (!ed) return;
|
||||
coeditingRegistry.exit(ed.document.uri);
|
||||
coeditingRegistry.syncContext(ed);
|
||||
}),
|
||||
vscode.window.onDidChangeActiveTextEditor((ed) => coeditingRegistry.syncContext(ed)),
|
||||
);
|
||||
coeditingRegistry.syncContext(vscode.window.activeTextEditor);
|
||||
|
||||
// --- F6: baseline data layer (Features #17 + #19) — workspace-INDEPENDENT ---
|
||||
// The two-pane vscode.diff VIEW was removed in #34 (F10's rendered preview is
|
||||
// the single review surface); only the baseline store survives, consumed by
|
||||
@@ -362,6 +390,7 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
||||
sidecarRouter,
|
||||
liveProgressUi,
|
||||
editorProposalController,
|
||||
coeditingRegistry,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user