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.toggleAttribution", "title": "Toggle Attribution", "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": {
"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": [
{
@@ -61,6 +67,16 @@
"command": "cowriting.reopenThread",
"group": "inline",
"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 { ThreadController } from "./threadController";
import { AttributionController } from "./attributionController";
import { ProposalController } from "./proposalController";
import { buildFingerprint } from "./anchorer";
const CHANNEL_NAME = "Cowriting (Cline SDK)";
export interface CowritingApi {
threadController: ThreadController;
attributionController: AttributionController;
proposalController: ProposalController;
}
export function activate(context: vscode.ExtensionContext): CowritingApi | undefined {
@@ -55,6 +58,9 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
"cowriting.editSelection",
"cowriting.toggleAttribution",
"cowriting.applyAgentEdit",
"cowriting.acceptProposal",
"cowriting.rejectProposal",
"cowriting.proposeAgentEdit",
]) {
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);
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
// suppressed centrally in the store (the sidecar is co-owned).
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;
threadController.handleExternalSidecarChange(uri);
attributionController.handleExternalSidecarChange(uri);
proposalController.handleExternalSidecarChange(uri);
};
watcher.onDidChange(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
// claude-code SDK turn (liveTurn.ts, INV-8) → the applyAgentEdit seam (INV-9).
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)) {
threadController.renderAll(doc);
attributionController.loadAll(doc);
proposalController.renderAll(doc);
}
};
vscode.workspace.textDocuments.forEach(renderIfOpen);
context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(renderIfOpen));
return { threadController, attributionController };
return { threadController, attributionController, proposalController };
}
export function deactivate(): void {