import * as vscode from "vscode"; import { fetchSdkSummary } from "./cline"; import { CoauthorStore } from "./store"; import { ThreadController } from "./threadController"; import { AttributionController } from "./attributionController"; const CHANNEL_NAME = "Cowriting (Cline SDK)"; export interface CowritingApi { threadController: ThreadController; attributionController: AttributionController; } export function activate(context: vscode.ExtensionContext): CowritingApi | undefined { // --- POC command (Feature #2), unchanged --- const output = vscode.window.createOutputChannel(CHANNEL_NAME); context.subscriptions.push(output); context.subscriptions.push( vscode.commands.registerCommand("cowriting.showClineSdkInfo", async () => { try { const summary = await fetchSdkSummary(); output.clear(); output.appendLine(`@cline/sdk version: ${summary.version}`); output.appendLine(`Builtin tools (${summary.tools.length}):`); for (const tool of summary.tools) output.appendLine(` • ${tool.id} — ${tool.description}`); output.show(true); await vscode.window.showInformationMessage( `Cline SDK ${summary.version} loaded — ${summary.tools.length} builtin tools. See the "${CHANNEL_NAME}" output channel.`, ); } catch (err) { const message = err instanceof Error ? err.message : String(err); output.appendLine(`Failed to drive @cline/sdk: ${message}`); output.show(true); await vscode.window.showErrorMessage(`Cowriting: failed to load @cline/sdk — ${message}`); } }), ); // --- F2: region-anchored threads (Feature #4) --- const root = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; if (!root) { // No folder open → nothing to anchor against, but every contributed // command must still exist: leave them unregistered and the palette // errors with "command not found" (#8). Register warning stubs instead; // opening a folder reloads the window, re-running activate with a root. const stub = () => void vscode.window.showWarningMessage( "Cowriting: open a folder first — coauthoring anchors threads and attribution to workspace files.", ); for (const command of [ "cowriting.createThread", "cowriting.reply", "cowriting.resolveThread", "cowriting.reopenThread", "cowriting.editSelection", "cowriting.toggleAttribution", "cowriting.applyAgentEdit", ]) { context.subscriptions.push(vscode.commands.registerCommand(command, stub)); } return undefined; } const store = new CoauthorStore(root); const threadController = new ThreadController(store, root); context.subscriptions.push(threadController); // --- F3: live attribution (Feature #6) --- const attributionController = new AttributionController(store, root); context.subscriptions.push(attributionController); // 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"); const onSidecar = (uri: vscode.Uri) => { if (store.consumeSelfWrite(uri.fsPath)) return; threadController.handleExternalSidecarChange(uri); attributionController.handleExternalSidecarChange(uri); }; watcher.onDidChange(onSidecar); watcher.onDidCreate(onSidecar); context.subscriptions.push(watcher); // The seam as a command (INV-9): the only machine-edit ingress, hidden from // the palette — for the host E2E harness and future SDK turn plumbing. context.subscriptions.push( vscode.commands.registerCommand( "cowriting.applyAgentEdit", (args: { uri: string; start: number; end: number; newText: string; model?: string; sessionId?: string; turnId?: string; }) => { const doc = vscode.workspace.textDocuments.find((d) => d.uri.toString() === args.uri); if (!doc) return Promise.resolve(false); const range = new vscode.Range(doc.positionAt(args.start), doc.positionAt(args.end)); const provenance = { kind: "agent" as const, id: "claude", agent: { sdk: "@cline/sdk", model: args.model ?? "sonnet", sessionId: args.sessionId ?? "" }, }; return attributionController.applyAgentEdit(doc, range, args.newText, provenance, { turnId: args.turnId }); }, ), ); // 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( vscode.commands.registerCommand("cowriting.editSelection", async () => { const editor = vscode.window.activeTextEditor; if ( !editor || editor.selection.isEmpty || editor.document.uri.scheme !== "file" || !editor.document.uri.fsPath.startsWith(root) ) { void vscode.window.showWarningMessage("Cowriting: select some text in a workspace document first."); return; } const instruction = await vscode.window.showInputBox({ prompt: "What should Claude do with the selection?", placeHolder: "e.g. tighten this paragraph", }); if (!instruction) return; if (editor.selection.isEmpty) { void vscode.window.showWarningMessage("Cowriting: select some text in a workspace document first."); return; } const document = editor.document; const selection = editor.selection; const expectedVersion = document.version; const selectedText = document.getText(selection); const turnId = `turn-${Date.now().toString(36)}`; try { await vscode.window.withProgress( { location: vscode.ProgressLocation.Notification, title: "Cowriting: asking Claude…" }, async () => { const { runEditTurn } = await import("./liveTurn"); const turn = await runEditTurn(instruction, selectedText); if (turn.replacement === "") { void vscode.window.showWarningMessage( "Cowriting: Claude returned an empty replacement — nothing was applied.", ); return; } const ok = await attributionController.applyAgentEdit( document, new vscode.Range(selection.start, selection.end), turn.replacement, { kind: "agent", id: "claude", agent: { sdk: "@cline/sdk", model: turn.model, sessionId: turn.sessionId }, }, { expectedVersion, turnId }, ); if (!ok) { void vscode.window.showWarningMessage( "Cowriting: the edit could not be applied (the document changed during the turn, or the editor rejected the edit) — nothing was changed.", ); } }, ); } catch (err) { const message = err instanceof Error ? err.message : String(err); void vscode.window.showErrorMessage(`Cowriting: Claude edit failed — ${message}`); } }), ); // Render threads + attributions for already-open editors, and on future opens. const renderIfOpen = (doc: vscode.TextDocument) => { if (doc.uri.scheme === "file" && doc.uri.fsPath.startsWith(root)) { threadController.renderAll(doc); attributionController.loadAll(doc); } }; vscode.workspace.textDocuments.forEach(renderIfOpen); context.subscriptions.push(vscode.workspace.onDidOpenTextDocument(renderIfOpen)); return { threadController, attributionController }; } export function deactivate(): void { // Disposables registered on the context handle cleanup. }