diff --git a/package.json b/package.json index e94f3a4..be51ccf 100644 --- a/package.json +++ b/package.json @@ -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" } ] } diff --git a/src/extension.ts b/src/extension.ts index f99eaf1..58c4d7c 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -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 {