feat: comments-first ask + comment→reply→offer→proposal loop (D19/D10/D8, PUC-8); sunset the input webview (spec §6.10)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+17
-107
@@ -15,13 +15,11 @@ import { TrackChangesPreviewController } from "./trackChangesPreview";
|
||||
import { EditFlow } from "./editFlow";
|
||||
import { LiveProgressUi } from "./liveProgressUi";
|
||||
import { EditorProposalController } from "./editorProposalController";
|
||||
import { isAuthorable, routeEdit, selectionRejection } from "./workspacePath";
|
||||
import { isAuthorable, routeEdit } from "./workspacePath";
|
||||
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;
|
||||
@@ -148,8 +146,6 @@ 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, coeditingRegistry);
|
||||
context.subscriptions.push(threadController);
|
||||
|
||||
// --- F3: live attribution (Feature #6) ---
|
||||
const attributionController = new AttributionController(sidecarRouter, root, versionGuard, coeditingRegistry);
|
||||
@@ -170,10 +166,17 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
||||
// `cowriting.editDocument` command, the instruction-prompt/progress-wrapped
|
||||
// "ask Claude" gesture (INV-10 gate), and the turn→proposal(s) cut
|
||||
// (`runEditAndPropose`, INV-39/40). Constructed BEFORE the review preview (which
|
||||
// delegates to it) and reachable from the thread controller too (Task 6). ---
|
||||
// delegates to it) and BEFORE the thread controller, which now consumes it too
|
||||
// (Task 6: the comment-loop's "make this edit" offer calls runEditAndPropose). ---
|
||||
const editFlow = new EditFlow(proposalController, attributionController, liveProgressUi, coeditingRegistry);
|
||||
context.subscriptions.push(editFlow);
|
||||
|
||||
// --- F2/F10 (Task 6, D19/D10/D8): comments-first ask + the comment→reply→
|
||||
// offer→proposal loop. Consumes editFlow (offer → pending proposals) and
|
||||
// liveProgressUi (the shared "asking Claude…" notification + output channel). ---
|
||||
const threadController = new ThreadController(sidecarRouter, root, versionGuard, coeditingRegistry, editFlow, liveProgressUi);
|
||||
context.subscriptions.push(threadController);
|
||||
|
||||
// --- F7/F10: the review preview is the single interactive review surface ---
|
||||
// Workspace-INDEPENDENT (works on any markdown doc, reuses the F6 baseline,
|
||||
// INV-20). Constructed AFTER attribution (reads F3 spans), proposals (routes
|
||||
@@ -307,109 +310,16 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
||||
),
|
||||
);
|
||||
|
||||
// F3 SLICE-5 → F4 SLICE-4: the live turn — selection + instruction → one
|
||||
// claude-code SDK turn (liveTurn.ts, INV-8) → a PENDING PROPOSAL (F4,
|
||||
// INV-10); the applyAgentEdit seam (INV-9) now fires only on accept.
|
||||
// Task 6 (D19): the comments-first ask replaces the old inline turn
|
||||
// plumbing here — the ask now IS a comment. threadController.askClaude()
|
||||
// opens a focused comment box on the current selection (or, with no
|
||||
// selection, a top-anchored whole-document thread); the comment→reply→
|
||||
// offer→proposal loop (ThreadController.respondInThread/makeThreadEdit)
|
||||
// takes it from there. Kept as its own command (rather than folded into
|
||||
// cowriting.edit) for the host E2E harness and the internal seams.
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand("cowriting.editSelection", async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
// F8: authoring works on any file: or untitled: doc (the router decides
|
||||
// where its artifact is stored). Each failure still names its real reason
|
||||
// (no editor / no selection / a non-{file,untitled} read-only view) — not
|
||||
// always "select some text" (#24's per-condition messaging).
|
||||
const reason = selectionRejection({
|
||||
hasEditor: !!editor,
|
||||
selectionEmpty: editor?.selection.isEmpty ?? true,
|
||||
scheme: editor?.document.uri.scheme ?? "",
|
||||
});
|
||||
if (reason) {
|
||||
void vscode.window.showWarningMessage(reason);
|
||||
return;
|
||||
}
|
||||
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
|
||||
// otherwise collapse the selection we act on (spec §6.5 PUC-1: anchor
|
||||
// captured pre-turn so mid-turn edits can't skew it).
|
||||
const selectedText = document.getText(selection);
|
||||
const fp = buildFingerprint(document.getText(), {
|
||||
start: document.offsetAt(selection.start),
|
||||
end: document.offsetAt(selection.end),
|
||||
});
|
||||
// The instruction prompt is the multi-line split-below webview box (shared
|
||||
// with the document case, via EditFlow); the document above keeps the
|
||||
// selection highlighted while it's open. selectedText/fp were captured
|
||||
// above, so moving focus to the box doesn't affect what we edit.
|
||||
const instruction = await editFlow.askEditInstruction("Ask Claude to Edit This Selection");
|
||||
if (!instruction) return;
|
||||
const turnId = `turn-${Date.now().toString(36)}`;
|
||||
try {
|
||||
await vscode.window.withProgress(
|
||||
{
|
||||
location: vscode.ProgressLocation.Notification,
|
||||
title: "Cowriting: asking Claude…",
|
||||
cancellable: true,
|
||||
},
|
||||
async (progress, token) => {
|
||||
const { runEditTurn } = await import("./liveTurn");
|
||||
const ui = liveProgressUi.begin(instruction, progress, token);
|
||||
let turn;
|
||||
try {
|
||||
turn = await runEditTurn(instruction, selectedText, {
|
||||
onProgress: ui.onProgress,
|
||||
signal: ui.signal,
|
||||
});
|
||||
} catch (err) {
|
||||
// #60 (INV-47): a user cancel surfaces as "cancelled", not a failure.
|
||||
if (token.isCancellationRequested) {
|
||||
void vscode.window.showInformationMessage("Cowriting: Claude edit cancelled.");
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
if (turn.replacement === "") {
|
||||
void vscode.window.showWarningMessage(
|
||||
"Cowriting: Claude returned an empty replacement — nothing was proposed.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (turn.replacement === selectedText) {
|
||||
void vscode.window.showInformationMessage(
|
||||
"Cowriting: Claude proposed no change to the selection.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
// F4 (INV-10): the turn ends in a PROPOSAL, not a buffer mutation.
|
||||
// The seam now fires only on accept (ProposalController, INV-9).
|
||||
const id = await proposalController.propose(
|
||||
document,
|
||||
fp,
|
||||
turn.replacement,
|
||||
{
|
||||
kind: "agent",
|
||||
id: "claude",
|
||||
agent: { sdk: "@cline/sdk", model: turn.model, sessionId: turn.sessionId },
|
||||
},
|
||||
{ turnId, instruction },
|
||||
);
|
||||
if (id) {
|
||||
void vscode.window.showInformationMessage(
|
||||
"Cowriting: Claude proposed an edit — review the diff at the highlighted range (✓ accept / ✗ reject).",
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
void vscode.window.showErrorMessage(`Cowriting: Claude edit failed — ${message}`);
|
||||
}
|
||||
await threadController.askClaude();
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user