F4 SLICE-3: wire ProposalController — contributes, watcher fan-out, propose command, stubs, API (#12)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-10 22:05:16 -07:00
parent 4efd34ffda
commit 4688ac8bbd
2 changed files with 56 additions and 3 deletions
+18 -2
View File
@@ -30,11 +30,17 @@
{ "command": "cowriting.reopenThread", "title": "Reopen Thread", "category": "Cowriting" }, { "command": "cowriting.reopenThread", "title": "Reopen Thread", "category": "Cowriting" },
{ "command": "cowriting.toggleAttribution", "title": "Toggle Attribution", "category": "Cowriting" }, { "command": "cowriting.toggleAttribution", "title": "Toggle Attribution", "category": "Cowriting" },
{ "command": "cowriting.applyAgentEdit", "title": "Apply Agent Edit (internal seam)", "category": "Cowriting" }, { "command": "cowriting.applyAgentEdit", "title": "Apply Agent Edit (internal seam)", "category": "Cowriting" },
{ "command": "cowriting.editSelection", "title": "Ask Claude to Edit Selection", "category": "Cowriting" } { "command": "cowriting.editSelection", "title": "Ask Claude to Edit Selection", "category": "Cowriting" },
{ "command": "cowriting.acceptProposal", "title": "✓ Accept Proposal", "category": "Cowriting" },
{ "command": "cowriting.rejectProposal", "title": "✗ Reject Proposal", "category": "Cowriting" },
{ "command": "cowriting.proposeAgentEdit", "title": "Propose Agent Edit (internal seam)", "category": "Cowriting" }
], ],
"menus": { "menus": {
"commandPalette": [ "commandPalette": [
{ "command": "cowriting.applyAgentEdit", "when": "false" } { "command": "cowriting.applyAgentEdit", "when": "false" },
{ "command": "cowriting.proposeAgentEdit", "when": "false" },
{ "command": "cowriting.acceptProposal", "when": "false" },
{ "command": "cowriting.rejectProposal", "when": "false" }
], ],
"editor/context": [ "editor/context": [
{ {
@@ -61,6 +67,16 @@
"command": "cowriting.reopenThread", "command": "cowriting.reopenThread",
"group": "inline", "group": "inline",
"when": "commentController == cowriting.threads && commentThread =~ /^resolved$/" "when": "commentController == cowriting.threads && commentThread =~ /^resolved$/"
},
{
"command": "cowriting.acceptProposal",
"group": "inline@1",
"when": "commentController == cowriting.proposals && commentThread =~ /^pending$/"
},
{
"command": "cowriting.rejectProposal",
"group": "inline@2",
"when": "commentController == cowriting.proposals"
} }
] ]
} }
+38 -1
View File
@@ -3,12 +3,15 @@ import { fetchSdkSummary } from "./cline";
import { CoauthorStore } from "./store"; import { CoauthorStore } from "./store";
import { ThreadController } from "./threadController"; import { ThreadController } from "./threadController";
import { AttributionController } from "./attributionController"; import { AttributionController } from "./attributionController";
import { ProposalController } from "./proposalController";
import { buildFingerprint } from "./anchorer";
const CHANNEL_NAME = "Cowriting (Cline SDK)"; const CHANNEL_NAME = "Cowriting (Cline SDK)";
export interface CowritingApi { export interface CowritingApi {
threadController: ThreadController; threadController: ThreadController;
attributionController: AttributionController; attributionController: AttributionController;
proposalController: ProposalController;
} }
export function activate(context: vscode.ExtensionContext): CowritingApi | undefined { export function activate(context: vscode.ExtensionContext): CowritingApi | undefined {
@@ -55,6 +58,9 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
"cowriting.editSelection", "cowriting.editSelection",
"cowriting.toggleAttribution", "cowriting.toggleAttribution",
"cowriting.applyAgentEdit", "cowriting.applyAgentEdit",
"cowriting.acceptProposal",
"cowriting.rejectProposal",
"cowriting.proposeAgentEdit",
]) { ]) {
context.subscriptions.push(vscode.commands.registerCommand(command, stub)); context.subscriptions.push(vscode.commands.registerCommand(command, stub));
} }
@@ -68,6 +74,10 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
const attributionController = new AttributionController(store, root); const attributionController = new AttributionController(store, root);
context.subscriptions.push(attributionController); context.subscriptions.push(attributionController);
// --- F4: propose/accept (Feature #12) ---
const proposalController = new ProposalController(store, attributionController, root);
context.subscriptions.push(proposalController);
// One SHARED sidecar watcher for both controllers; self-writes are // One SHARED sidecar watcher for both controllers; self-writes are
// suppressed centrally in the store (the sidecar is co-owned). // suppressed centrally in the store (the sidecar is co-owned).
const watcher = vscode.workspace.createFileSystemWatcher("**/.threads/**/*.json"); const watcher = vscode.workspace.createFileSystemWatcher("**/.threads/**/*.json");
@@ -75,6 +85,7 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
if (store.consumeSelfWrite(uri.fsPath)) return; if (store.consumeSelfWrite(uri.fsPath)) return;
threadController.handleExternalSidecarChange(uri); threadController.handleExternalSidecarChange(uri);
attributionController.handleExternalSidecarChange(uri); attributionController.handleExternalSidecarChange(uri);
proposalController.handleExternalSidecarChange(uri);
}; };
watcher.onDidChange(onSidecar); watcher.onDidChange(onSidecar);
watcher.onDidCreate(onSidecar); watcher.onDidCreate(onSidecar);
@@ -102,6 +113,31 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
), ),
); );
// The propose ingress as a command (spec §6.4): records a pending proposal,
// NEVER touches the document (INV-10) — for the host E2E harness.
context.subscriptions.push(
vscode.commands.registerCommand(
"cowriting.proposeAgentEdit",
(args: {
uri: string; start: number; end: number; newText: string;
model?: string; sessionId?: string; turnId?: string; instruction?: string;
}) => {
const doc = vscode.workspace.textDocuments.find((d) => d.uri.toString() === args.uri);
if (!doc) return Promise.resolve(undefined);
const fp = buildFingerprint(doc.getText(), { start: args.start, end: args.end });
const provenance = {
kind: "agent" as const,
id: "claude",
agent: { sdk: "@cline/sdk", model: args.model ?? "sonnet", sessionId: args.sessionId ?? "" },
};
return proposalController.propose(doc, fp, args.newText, provenance, {
turnId: args.turnId,
instruction: args.instruction,
});
},
),
);
// F3 SLICE-5: the live turn (PUC-2) — selection + instruction → one // F3 SLICE-5: the live turn (PUC-2) — selection + instruction → one
// claude-code SDK turn (liveTurn.ts, INV-8) → the applyAgentEdit seam (INV-9). // claude-code SDK turn (liveTurn.ts, INV-8) → the applyAgentEdit seam (INV-9).
context.subscriptions.push( context.subscriptions.push(
@@ -172,12 +208,13 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
if (doc.uri.scheme === "file" && doc.uri.fsPath.startsWith(root)) { if (doc.uri.scheme === "file" && doc.uri.fsPath.startsWith(root)) {
threadController.renderAll(doc); threadController.renderAll(doc);
attributionController.loadAll(doc); attributionController.loadAll(doc);
proposalController.renderAll(doc);
} }
}; };
vscode.workspace.textDocuments.forEach(renderIfOpen); vscode.workspace.textDocuments.forEach(renderIfOpen);
context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(renderIfOpen)); context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(renderIfOpen));
return { threadController, attributionController }; return { threadController, attributionController, proposalController };
} }
export function deactivate(): void { export function deactivate(): void {