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:
+6
-6
@@ -173,11 +173,11 @@
|
||||
},
|
||||
{
|
||||
"command": "cowriting.acceptAllProposals",
|
||||
"when": "editorLangId == markdown"
|
||||
"when": "editorLangId == markdown && cowriting.isCoediting"
|
||||
},
|
||||
{
|
||||
"command": "cowriting.rejectAllProposals",
|
||||
"when": "editorLangId == markdown"
|
||||
"when": "editorLangId == markdown && cowriting.isCoediting"
|
||||
},
|
||||
{
|
||||
"command": "cowriting.proposalAcceptMenu",
|
||||
@@ -208,7 +208,7 @@
|
||||
"editor/title/context": [
|
||||
{
|
||||
"command": "cowriting.edit",
|
||||
"when": "resourceLangId == markdown",
|
||||
"when": "resourceLangId == markdown && cowriting.isCoediting",
|
||||
"group": "1_cowriting@1"
|
||||
},
|
||||
{
|
||||
@@ -227,12 +227,12 @@
|
||||
"editor/context": [
|
||||
{
|
||||
"command": "cowriting.edit",
|
||||
"when": "editorLangId == markdown && (resourceScheme == file || resourceScheme == untitled)",
|
||||
"when": "editorLangId == markdown && (resourceScheme == file || resourceScheme == untitled) && cowriting.isCoediting",
|
||||
"group": "1_cowriting@1"
|
||||
},
|
||||
{
|
||||
"command": "cowriting.createThread",
|
||||
"when": "editorHasSelection && (resourceScheme == file || resourceScheme == untitled)",
|
||||
"when": "editorHasSelection && (resourceScheme == file || resourceScheme == untitled) && cowriting.isCoediting",
|
||||
"group": "1_cowriting@2"
|
||||
},
|
||||
{
|
||||
@@ -277,7 +277,7 @@
|
||||
"command": "cowriting.edit",
|
||||
"key": "ctrl+alt+e",
|
||||
"mac": "cmd+alt+e",
|
||||
"when": "editorTextFocus && editorLangId == markdown"
|
||||
"when": "editorTextFocus && editorLangId == markdown && cowriting.isCoediting"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@ import { minimizeReplace, PendingEditRegistry } from "./pendingEdits";
|
||||
import type { VersionGuard } from "./versionGuard";
|
||||
import { isAuthorable } from "./workspacePath";
|
||||
import type { AuthorSpan } from "./trackChangesModel";
|
||||
import type { CoeditingRegistry } from "./coeditingRegistry";
|
||||
|
||||
/** Test-facing snapshot of live attribution state for a document. */
|
||||
export interface RenderedSpan {
|
||||
@@ -69,6 +70,7 @@ export class AttributionController implements vscode.Disposable {
|
||||
private readonly store: SidecarRouter,
|
||||
private readonly rootDir: string | undefined,
|
||||
private readonly guard: VersionGuard,
|
||||
private readonly registry: CoeditingRegistry,
|
||||
) {
|
||||
this.disposables.push(this.statusItem, this.output, this.applyEmitter);
|
||||
this.disposables.push(
|
||||
@@ -104,6 +106,7 @@ export class AttributionController implements vscode.Disposable {
|
||||
/** Load the sidecar and re-resolve every attribution (live span | orphan). */
|
||||
loadAll(document: vscode.TextDocument): void {
|
||||
if (!this.isTracked(document)) return;
|
||||
if (!this.registry.isCoediting(document.uri)) return;
|
||||
const docPath = this.keyOf(document);
|
||||
const s = this.state(docPath);
|
||||
const artifact = this.store.load(docPath);
|
||||
@@ -145,6 +148,7 @@ export class AttributionController implements vscode.Disposable {
|
||||
|
||||
private onDidChange(e: vscode.TextDocumentChangeEvent): void {
|
||||
if (!this.isTracked(e.document) || e.contentChanges.length === 0) return;
|
||||
if (!this.registry.isCoediting(e.document.uri)) return;
|
||||
const docPath = this.keyOf(e.document);
|
||||
if (!e.document.isDirty && this.matchesDisk(e.document)) {
|
||||
// Disk sync (revert / external reload): buffer now equals the file on
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
+27
-5
@@ -19,6 +19,8 @@ import { CoeditingRegistry } from "./coeditingRegistry";
|
||||
import { ScmSurfaceController } from "./scmSurface";
|
||||
|
||||
const CHANNEL_NAME = "Cowriting (Cline SDK)";
|
||||
/** The exact warning copy for every gated edit gesture (INV-10). */
|
||||
const NOT_COEDITING_WARNING = "Run ✦ Coedit this Document with Claude first.";
|
||||
|
||||
export interface CowritingApi {
|
||||
threadController: ThreadController;
|
||||
@@ -144,16 +146,22 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
||||
// F5 (INV-16): one shared guard — newer-major sidecars are read-only, one
|
||||
// warning per doc across the three co-owning controllers.
|
||||
const versionGuard = new VersionGuard(sidecarRouter);
|
||||
const threadController = new ThreadController(sidecarRouter, root, versionGuard);
|
||||
const threadController = new ThreadController(sidecarRouter, root, versionGuard, coeditingRegistry);
|
||||
context.subscriptions.push(threadController);
|
||||
|
||||
// --- F3: live attribution (Feature #6) ---
|
||||
const attributionController = new AttributionController(sidecarRouter, root, versionGuard);
|
||||
const attributionController = new AttributionController(sidecarRouter, root, versionGuard, coeditingRegistry);
|
||||
context.subscriptions.push(attributionController);
|
||||
|
||||
// --- F4: propose/accept (Feature #12) — constructed before the preview so F10
|
||||
// can route ✓/✗ through it ---
|
||||
const proposalController = new ProposalController(sidecarRouter, attributionController, root, versionGuard);
|
||||
const proposalController = new ProposalController(
|
||||
sidecarRouter,
|
||||
attributionController,
|
||||
root,
|
||||
versionGuard,
|
||||
coeditingRegistry,
|
||||
);
|
||||
context.subscriptions.push(proposalController);
|
||||
|
||||
// --- F7/F10: the review preview is the single interactive review surface ---
|
||||
@@ -166,12 +174,18 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
||||
attributionController,
|
||||
proposalController,
|
||||
liveProgressUi,
|
||||
coeditingRegistry,
|
||||
);
|
||||
context.subscriptions.push(trackChangesPreviewController);
|
||||
|
||||
// --- F12 (#64): the editor surface — optimistic-apply proposals into the buffer,
|
||||
// decorate the diff, and provide Accept ▾/Reject ▾ CodeLens (INV-48/49/52/53). ---
|
||||
const editorProposalController = new EditorProposalController(proposalController, diffViewController, attributionController);
|
||||
const editorProposalController = new EditorProposalController(
|
||||
proposalController,
|
||||
diffViewController,
|
||||
attributionController,
|
||||
coeditingRegistry,
|
||||
);
|
||||
context.subscriptions.push(editorProposalController);
|
||||
|
||||
// #46 (INV-42): accept every pending proposal on the active doc in one gesture
|
||||
@@ -289,6 +303,11 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
||||
}
|
||||
if (!editor) return; // unreachable once reason is null, but narrows the type
|
||||
const document = editor.document;
|
||||
// INV-10: a non-entered doc gets the gate warning, not a turn.
|
||||
if (!coeditingRegistry.isCoediting(document.uri)) {
|
||||
void vscode.window.showWarningMessage(NOT_COEDITING_WARNING);
|
||||
return;
|
||||
}
|
||||
const selection = editor.selection; // non-empty (selectionRejection guaranteed it)
|
||||
// Capture the selection text + anchor BEFORE prompting — the inline prompt
|
||||
// moves the cursor to the document top (where its box anchors), which would
|
||||
@@ -394,8 +413,11 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
||||
);
|
||||
|
||||
// Render threads + attributions for already-open editors, and on future opens.
|
||||
// INV-10: gated on the registry too — belt-and-suspenders with the per-controller
|
||||
// gates (renderAll/loadAll already early-return), but keeps the intent explicit
|
||||
// at the call site and avoids the no-op calls for a non-entered doc.
|
||||
const renderIfOpen = (doc: vscode.TextDocument) => {
|
||||
if (isAuthorable(doc.uri.scheme)) {
|
||||
if (isAuthorable(doc.uri.scheme) && coeditingRegistry.isCoediting(doc.uri)) {
|
||||
threadController.renderAll(doc);
|
||||
attributionController.loadAll(doc);
|
||||
proposalController.renderAll(doc);
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { AttributionController } from "./attributionController";
|
||||
import type { VersionGuard } from "./versionGuard";
|
||||
import { isAuthorable } from "./workspacePath";
|
||||
import { wordEditHunks, type ProposalView } from "./trackChangesModel";
|
||||
import type { CoeditingRegistry } from "./coeditingRegistry";
|
||||
|
||||
/** Test-facing snapshot of what is currently rendered for a document. */
|
||||
export interface RenderedProposal {
|
||||
@@ -56,6 +57,7 @@ export class ProposalController implements vscode.Disposable {
|
||||
private readonly attribution: AttributionController,
|
||||
private readonly rootDir: string | undefined,
|
||||
private readonly guard: VersionGuard,
|
||||
private readonly registry: CoeditingRegistry,
|
||||
) {
|
||||
this.disposables.push(this.statusItem);
|
||||
this.disposables.push(this.onDidChangeProposalsEmitter);
|
||||
@@ -459,7 +461,13 @@ export class ProposalController implements vscode.Disposable {
|
||||
|
||||
// ---- PUC-4: load / external change / resolve-or-flag --------------------------------
|
||||
|
||||
/** Load + (re)render every pending proposal at its resolved anchor (or flagged). */
|
||||
/** Load + (re)render every pending proposal at its resolved anchor (or flagged).
|
||||
* INV-10: the RENDER portion (live/unresolved recompute, the status bar, and
|
||||
* the onDidChangeProposals fire that drives F12's optimistic-apply/CodeLens/
|
||||
* decorations) is gated on coediting — `propose()` itself stays ungated (it
|
||||
* always persists), and so does the state LOAD below: `acceptById`/`rejectById`
|
||||
* key off `this.docs` (populated here) regardless of coediting, since deciding
|
||||
* an already-persisted proposal is not a "rendering" surface. */
|
||||
renderAll(document: vscode.TextDocument): void {
|
||||
if (!this.isTracked(document)) return;
|
||||
const docPath = this.keyOf(document);
|
||||
@@ -467,6 +475,7 @@ export class ProposalController implements vscode.Disposable {
|
||||
state.artifact = this.store.load(docPath) ?? emptyArtifact(docPath);
|
||||
state.live.clear();
|
||||
state.unresolved.clear();
|
||||
if (!this.registry.isCoediting(document.uri)) return;
|
||||
const text = document.getText();
|
||||
for (const proposal of state.artifact.proposals) {
|
||||
const fp = state.artifact.anchors[proposal.anchorId]?.fingerprint;
|
||||
|
||||
+40
-6
@@ -15,6 +15,7 @@ import { gitUserEmail } from "./identity";
|
||||
import { addThread, appendMessage, setStatus } from "./threadModel";
|
||||
import type { VersionGuard } from "./versionGuard";
|
||||
import { isAuthorable } from "./workspacePath";
|
||||
import type { CoeditingRegistry } from "./coeditingRegistry";
|
||||
|
||||
/** Test-facing snapshot of what is currently rendered for a document. */
|
||||
export interface RenderedThread {
|
||||
@@ -41,18 +42,24 @@ export class ThreadController implements vscode.Disposable {
|
||||
private readonly disposables: vscode.Disposable[] = [];
|
||||
private readonly docs = new Map<string, DocState>(); // keyed by docPath
|
||||
|
||||
/** Kept as ONE object; re-assigned on every registry change so VS Code re-queries
|
||||
* (spec §6.4 v0.2.1 — reassignment is the API's only "ranges changed" signal). */
|
||||
private readonly rangeProvider: vscode.CommentingRangeProvider = {
|
||||
provideCommentingRanges: (document) => {
|
||||
if (!isAuthorable(document.uri.scheme)) return [];
|
||||
if (!this.registry.isCoediting(document.uri)) return [];
|
||||
return [new vscode.Range(0, 0, Math.max(0, document.lineCount - 1), 0)];
|
||||
},
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly store: SidecarRouter,
|
||||
private readonly rootDir: string | undefined,
|
||||
private readonly guard: VersionGuard,
|
||||
private readonly registry: CoeditingRegistry,
|
||||
) {
|
||||
this.controller = vscode.comments.createCommentController("cowriting.threads", "Coauthoring Threads");
|
||||
this.controller.commentingRangeProvider = {
|
||||
provideCommentingRanges: (document) => {
|
||||
if (!isAuthorable(document.uri.scheme)) return [];
|
||||
return [new vscode.Range(0, 0, Math.max(0, document.lineCount - 1), 0)];
|
||||
},
|
||||
};
|
||||
this.controller.commentingRangeProvider = this.rangeProvider;
|
||||
this.disposables.push(this.controller);
|
||||
|
||||
this.disposables.push(
|
||||
@@ -66,6 +73,20 @@ export class ThreadController implements vscode.Disposable {
|
||||
),
|
||||
vscode.workspace.onDidChangeTextDocument((e) => this.onDidChange(e)),
|
||||
vscode.workspace.onDidSaveTextDocument((d) => this.onDidSave(d)),
|
||||
// INV-10 (PUC-7): force VS Code to re-query commenting ranges on every gate
|
||||
// change, and hide/restore rendered threads — exit hides only (dispose the
|
||||
// rendered vsThreads), re-enter restores (renderAll reloads from the
|
||||
// sidecar, which is the durable source of truth regardless of gate state).
|
||||
this.registry.onDidChange(({ uri, coediting }) => {
|
||||
this.controller.commentingRangeProvider = this.rangeProvider;
|
||||
const doc = vscode.workspace.textDocuments.find((d) => d.uri.toString() === uri);
|
||||
if (!doc) return;
|
||||
if (coediting) {
|
||||
this.renderAll(doc);
|
||||
} else {
|
||||
this.disposeRendered(this.keyOf(doc));
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,6 +132,7 @@ export class ThreadController implements vscode.Disposable {
|
||||
async createThreadOnSelection(firstBody = "New thread"): Promise<string | undefined> {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor || editor.selection.isEmpty || !isAuthorable(editor.document.uri.scheme)) return undefined;
|
||||
if (!this.registry.isCoediting(editor.document.uri)) return undefined;
|
||||
if (this.guard.isReadOnly(this.keyOf(editor.document))) return undefined;
|
||||
const document = editor.document;
|
||||
const state = this.ensureState(document);
|
||||
@@ -152,6 +174,7 @@ export class ThreadController implements vscode.Disposable {
|
||||
|
||||
/** Load + (re)render every thread for a document at its resolved anchor (or orphaned). */
|
||||
renderAll(document: vscode.TextDocument): void {
|
||||
if (!this.registry.isCoediting(document.uri)) return;
|
||||
const docPath = this.keyOf(document);
|
||||
const state = this.ensureState(document);
|
||||
// fresh artifact from disk (reload / external change)
|
||||
@@ -269,6 +292,17 @@ export class ThreadController implements vscode.Disposable {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** PUC-7 (INV-10): exit hides only — dispose the rendered vsThreads for a doc
|
||||
* without touching its sidecar-backed artifact (re-enter restores via renderAll). */
|
||||
private disposeRendered(docPath: string): void {
|
||||
const state = this.docs.get(docPath);
|
||||
if (!state) return;
|
||||
for (const vsThread of state.vsThreads.values()) vsThread.dispose();
|
||||
state.vsThreads.clear();
|
||||
state.live.clear();
|
||||
state.orphaned.clear();
|
||||
}
|
||||
|
||||
// ---- test-facing surface --------------------------------------------------------
|
||||
|
||||
getRendered(docPath: string): RenderedThread[] {
|
||||
|
||||
@@ -19,6 +19,10 @@ import { isAuthorable } from "./workspacePath";
|
||||
import type { EditTurnResult, RunEditTurnOptions } from "./liveTurn";
|
||||
import type { LiveProgressUi } from "./liveProgressUi";
|
||||
import { promptEditInstruction } from "./editInstructionInput";
|
||||
import type { CoeditingRegistry } from "./coeditingRegistry";
|
||||
|
||||
/** The exact warning copy for every gated edit gesture (INV-10). */
|
||||
const NOT_COEDITING_WARNING = "Run ✦ Coedit this Document with Claude first.";
|
||||
|
||||
/**
|
||||
* F11: a host edit turn (selection/document text + instruction → rewrite).
|
||||
@@ -83,6 +87,7 @@ export class TrackChangesPreviewController implements vscode.Disposable {
|
||||
private readonly attribution: AttributionController,
|
||||
private readonly proposals: ProposalController,
|
||||
private readonly liveProgressUi: LiveProgressUi,
|
||||
private readonly registry: CoeditingRegistry,
|
||||
) {
|
||||
this.disposables.push(
|
||||
// F11 (SLICE-5): the editor/title gateway passes the tab's resource Uri;
|
||||
@@ -247,6 +252,13 @@ export class TrackChangesPreviewController implements vscode.Disposable {
|
||||
* the result as F4 proposal(s). UI wrapper around `runEditAndPropose`.
|
||||
*/
|
||||
private async askClaude(document: vscode.TextDocument, target: EditTarget): Promise<void> {
|
||||
// INV-10: the choke point for BOTH the editDocument command and the webview's
|
||||
// askClaude toolbar intent (either scope) — a non-entered doc gets the warning,
|
||||
// not a turn.
|
||||
if (!this.registry.isCoediting(document.uri)) {
|
||||
void vscode.window.showWarningMessage(NOT_COEDITING_WARNING);
|
||||
return;
|
||||
}
|
||||
// Both scopes use the same multi-line split-below webview box; only the header
|
||||
// (and the downstream proposal logic) differs. For a selection the document
|
||||
// above keeps the selection highlighted while the box is open.
|
||||
|
||||
@@ -18,6 +18,9 @@ async function openDoc(): Promise<vscode.TextDocument> {
|
||||
const uri = vscode.Uri.file(path.join(WS, DOC_REL));
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
// INV-10: attribution tracking is gated on coediting (Task 4) — idempotent
|
||||
// across this order-dependent suite's repeated openDoc() calls.
|
||||
await vscode.commands.executeCommand("cowriting.coeditDocument");
|
||||
return doc;
|
||||
}
|
||||
async function getApi(): Promise<CowritingApi> {
|
||||
|
||||
@@ -34,6 +34,9 @@ suite("F10 review preview — pending Claude proposal renders cw-ins-claude in t
|
||||
const uri = vscode.Uri.file(abs);
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
// INV-10: entering coediting is required for attribution tracking + F12
|
||||
// optimistic-apply to fire — this suite never entered it (pre-migration).
|
||||
await vscode.commands.executeCommand("cowriting.coeditDocument");
|
||||
await settle();
|
||||
const api = await getApi();
|
||||
const key = uri.toString();
|
||||
|
||||
@@ -32,4 +32,27 @@ suite("coediting registry (PUC-7)", () => {
|
||||
assert.strictEqual(api.coeditingRegistry.isCoediting(doc.uri), false);
|
||||
assert.strictEqual(api.scmSurfaceController.changeCount(doc.uri.toString()), 0);
|
||||
});
|
||||
|
||||
// Task 4 (INV-10, PUC-7): the no-hijack scenario — a non-entered doc gets no
|
||||
// thread-creation surface at all; entering unlocks it; exiting hides the
|
||||
// rendered thread (not the sidecar-backed data); re-entering restores it.
|
||||
test("non-entered doc gets no surfaces; exit detaches; re-enter restores threads", async () => {
|
||||
const api = await activateApi();
|
||||
const doc = await vscode.workspace.openTextDocument({ language: "markdown", content: "# a\n\npara\n" });
|
||||
const ed = await vscode.window.showTextDocument(doc);
|
||||
// not entered → no thread creation
|
||||
ed.selection = new vscode.Selection(2, 0, 2, 4);
|
||||
const before = await api.threadController.createThreadOnSelection("hi");
|
||||
assert.strictEqual(before, undefined);
|
||||
// entered → works
|
||||
await vscode.commands.executeCommand("cowriting.coeditDocument");
|
||||
const id = await api.threadController.createThreadOnSelection("hi");
|
||||
assert.ok(id);
|
||||
// exit → rendered thread set empty; re-enter → restored from sidecar
|
||||
await vscode.commands.executeCommand("cowriting.stopCoediting");
|
||||
assert.strictEqual(api.threadController.getRendered(api.proposalController.keyFor(doc)).length, 0);
|
||||
await vscode.commands.executeCommand("cowriting.coeditDocument");
|
||||
await settle();
|
||||
assert.strictEqual(api.threadController.getRendered(api.proposalController.keyFor(doc)).length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,6 +17,8 @@ async function open(docRel: string): Promise<vscode.TextDocument> {
|
||||
const uri = vscode.Uri.file(path.join(WS, docRel));
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
// INV-10: thread creation/rendering is gated on coediting (Task 4).
|
||||
await vscode.commands.executeCommand("cowriting.coeditDocument");
|
||||
return doc;
|
||||
}
|
||||
async function getApi(): Promise<CowritingApi> {
|
||||
|
||||
@@ -21,6 +21,9 @@ async function freshDoc(rel: string, body: string): Promise<{ doc: vscode.TextDo
|
||||
const uri = vscode.Uri.file(abs);
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
// INV-10: proposal rendering (F12 optimistic-apply, driven by
|
||||
// onDidChangeProposals) is gated on coediting (Task 4).
|
||||
await vscode.commands.executeCommand("cowriting.coeditDocument");
|
||||
await settle();
|
||||
return { doc, key: uri.toString() };
|
||||
}
|
||||
|
||||
@@ -20,6 +20,12 @@ async function freshDoc(rel: string, body: string): Promise<{ doc: vscode.TextDo
|
||||
fs.writeFileSync(abs, body, "utf8");
|
||||
const uri = vscode.Uri.file(abs);
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
// INV-10: cowriting.editDocument now warns instead of editing a non-entered
|
||||
// doc (Task 4) — enter it here (briefly making it active) so the tests below
|
||||
// exercise the routing/targeting behavior, not the gate. The caller
|
||||
// re-establishes whichever doc it wants active afterward.
|
||||
await vscode.window.showTextDocument(doc);
|
||||
await vscode.commands.executeCommand("cowriting.coeditDocument");
|
||||
return { doc, key: uri.toString() };
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ async function freshDoc(rel: string, body: string): Promise<vscode.TextDocument>
|
||||
fs.writeFileSync(abs, body, "utf8");
|
||||
const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(abs));
|
||||
await vscode.window.showTextDocument(doc);
|
||||
// INV-10: attribution tracking (accept's per-word attribution, INV-40) is
|
||||
// gated on coediting (Task 4).
|
||||
await vscode.commands.executeCommand("cowriting.coeditDocument");
|
||||
await settle();
|
||||
return doc;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ async function freshDoc(rel: string, body: string): Promise<vscode.TextDocument>
|
||||
fs.writeFileSync(abs, body, "utf8");
|
||||
const doc = await vscode.workspace.openTextDocument(vscode.Uri.file(abs));
|
||||
await vscode.window.showTextDocument(doc);
|
||||
// INV-10: proposal rendering (F12 optimistic-apply, driven by
|
||||
// onDidChangeProposals) is gated on coediting (Task 4).
|
||||
await vscode.commands.executeCommand("cowriting.coeditDocument");
|
||||
await settle();
|
||||
return doc;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ suite("F8 out-of-workspace authoring (host E2E — programmatic seam, no LLM)",
|
||||
const uri = vscode.Uri.file(outsidePath);
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
// INV-10: attribution tracking + thread creation are gated on coediting (Task 4).
|
||||
await vscode.commands.executeCommand("cowriting.coeditDocument");
|
||||
await settle();
|
||||
const api = await getApi();
|
||||
const key = uri.toString();
|
||||
@@ -80,6 +82,8 @@ suite("F8 out-of-workspace authoring (host E2E — programmatic seam, no LLM)",
|
||||
const TARGET = "The untitled draft sentence.";
|
||||
const untitled = await vscode.workspace.openTextDocument({ content: `${TARGET}\n`, language: "markdown" });
|
||||
await vscode.window.showTextDocument(untitled);
|
||||
// INV-10: attribution tracking is gated on coediting (Task 4).
|
||||
await vscode.commands.executeCommand("cowriting.coeditDocument");
|
||||
await settle();
|
||||
const api = await getApi();
|
||||
const key = untitled.uri.toString();
|
||||
|
||||
@@ -18,6 +18,10 @@ async function openDoc(): Promise<vscode.TextDocument> {
|
||||
const uri = vscode.Uri.file(path.join(WS, DOC_REL));
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
// INV-10: proposal rendering (and the F12 optimistic-apply it drives via
|
||||
// onDidChangeProposals) is gated on coediting (Task 4) — idempotent across
|
||||
// this order-dependent suite's repeated openDoc() calls.
|
||||
await vscode.commands.executeCommand("cowriting.coeditDocument");
|
||||
return doc;
|
||||
}
|
||||
async function getApi(): Promise<CowritingApi> {
|
||||
|
||||
@@ -18,6 +18,9 @@ async function openSample(): Promise<vscode.TextDocument> {
|
||||
const uri = vscode.Uri.file(path.join(WS, DOC_REL));
|
||||
const doc = await vscode.workspace.openTextDocument(uri);
|
||||
await vscode.window.showTextDocument(doc);
|
||||
// INV-10: thread creation/rendering is gated on coediting (Task 4) — idempotent
|
||||
// across this order-dependent suite's repeated openSample() calls.
|
||||
await vscode.commands.executeCommand("cowriting.coeditDocument");
|
||||
return doc;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user