feat(#60): wire live progress + cancel into both Ask-Claude call sites
editSelection (extension.ts) and preview askClaude (trackChangesPreview.ts) both run the turn cancellable, relay TurnProgress via the shared LiveProgressUi, and thread onProgress/signal through runEditAndPropose. The EditTurn seam widens to accept opts (back-compat: existing stubs ignore it). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+30
-3
@@ -11,6 +11,7 @@ import { GlobalSidecarStore } from "./globalSidecarStore";
|
|||||||
import { SidecarRouter } from "./sidecarRouter";
|
import { SidecarRouter } from "./sidecarRouter";
|
||||||
import { DiffViewController } from "./diffViewController";
|
import { DiffViewController } from "./diffViewController";
|
||||||
import { TrackChangesPreviewController } from "./trackChangesPreview";
|
import { TrackChangesPreviewController } from "./trackChangesPreview";
|
||||||
|
import { LiveProgressUi } from "./liveProgressUi";
|
||||||
import { isAuthorable, selectionRejection } from "./workspacePath";
|
import { isAuthorable, selectionRejection } from "./workspacePath";
|
||||||
|
|
||||||
const CHANNEL_NAME = "Cowriting (Cline SDK)";
|
const CHANNEL_NAME = "Cowriting (Cline SDK)";
|
||||||
@@ -23,12 +24,18 @@ export interface CowritingApi {
|
|||||||
diffViewController: DiffViewController;
|
diffViewController: DiffViewController;
|
||||||
trackChangesPreviewController: TrackChangesPreviewController;
|
trackChangesPreviewController: TrackChangesPreviewController;
|
||||||
sidecarRouter: SidecarRouter;
|
sidecarRouter: SidecarRouter;
|
||||||
|
liveProgressUi: LiveProgressUi;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function activate(context: vscode.ExtensionContext): CowritingApi | undefined {
|
export function activate(context: vscode.ExtensionContext): CowritingApi | undefined {
|
||||||
// --- POC command (Feature #2), unchanged ---
|
// --- POC command (Feature #2), unchanged ---
|
||||||
const output = vscode.window.createOutputChannel(CHANNEL_NAME);
|
const output = vscode.window.createOutputChannel(CHANNEL_NAME);
|
||||||
context.subscriptions.push(output);
|
context.subscriptions.push(output);
|
||||||
|
|
||||||
|
// #60: shared live-progress UI (notification activity line + "Cowriting: Claude"
|
||||||
|
// OutputChannel) for both Ask-Claude entry points.
|
||||||
|
const liveProgressUi = new LiveProgressUi();
|
||||||
|
context.subscriptions.push(liveProgressUi);
|
||||||
context.subscriptions.push(
|
context.subscriptions.push(
|
||||||
vscode.commands.registerCommand("cowriting.showClineSdkInfo", async () => {
|
vscode.commands.registerCommand("cowriting.showClineSdkInfo", async () => {
|
||||||
try {
|
try {
|
||||||
@@ -104,6 +111,7 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
|||||||
context.extensionUri,
|
context.extensionUri,
|
||||||
attributionController,
|
attributionController,
|
||||||
proposalController,
|
proposalController,
|
||||||
|
liveProgressUi,
|
||||||
);
|
);
|
||||||
context.subscriptions.push(trackChangesPreviewController);
|
context.subscriptions.push(trackChangesPreviewController);
|
||||||
|
|
||||||
@@ -230,10 +238,28 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
|||||||
const turnId = `turn-${Date.now().toString(36)}`;
|
const turnId = `turn-${Date.now().toString(36)}`;
|
||||||
try {
|
try {
|
||||||
await vscode.window.withProgress(
|
await vscode.window.withProgress(
|
||||||
{ location: vscode.ProgressLocation.Notification, title: "Cowriting: asking Claude…" },
|
{
|
||||||
async () => {
|
location: vscode.ProgressLocation.Notification,
|
||||||
|
title: "Cowriting: asking Claude…",
|
||||||
|
cancellable: true,
|
||||||
|
},
|
||||||
|
async (progress, token) => {
|
||||||
const { runEditTurn } = await import("./liveTurn");
|
const { runEditTurn } = await import("./liveTurn");
|
||||||
const turn = await runEditTurn(instruction, selectedText);
|
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 === "") {
|
if (turn.replacement === "") {
|
||||||
void vscode.window.showWarningMessage(
|
void vscode.window.showWarningMessage(
|
||||||
"Cowriting: Claude returned an empty replacement — nothing was proposed.",
|
"Cowriting: Claude returned an empty replacement — nothing was proposed.",
|
||||||
@@ -292,6 +318,7 @@ export function activate(context: vscode.ExtensionContext): CowritingApi | undef
|
|||||||
diffViewController,
|
diffViewController,
|
||||||
trackChangesPreviewController,
|
trackChangesPreviewController,
|
||||||
sidecarRouter,
|
sidecarRouter,
|
||||||
|
liveProgressUi,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,10 +16,15 @@ import type { ProposalController } from "./proposalController";
|
|||||||
import { renderReview, renderPlain, diffBlocks, diffToBlockHunks, type BlockOp } from "./trackChangesModel";
|
import { renderReview, renderPlain, diffBlocks, diffToBlockHunks, type BlockOp } from "./trackChangesModel";
|
||||||
import { buildFingerprint } from "./anchorer";
|
import { buildFingerprint } from "./anchorer";
|
||||||
import { isAuthorable } from "./workspacePath";
|
import { isAuthorable } from "./workspacePath";
|
||||||
import type { EditTurnResult } from "./liveTurn";
|
import type { EditTurnResult, RunEditTurnOptions } from "./liveTurn";
|
||||||
|
import type { LiveProgressUi } from "./liveProgressUi";
|
||||||
|
|
||||||
/** F11: a host edit turn (selection/document text + instruction → rewrite). Injectable for tests. */
|
/**
|
||||||
type EditTurn = (instruction: string, text: string) => Promise<EditTurnResult>;
|
* F11: a host edit turn (selection/document text + instruction → rewrite).
|
||||||
|
* Injectable for tests. #60: accepts optional turn options (onProgress/signal);
|
||||||
|
* the arg is optional so existing test stubs that ignore it stay valid.
|
||||||
|
*/
|
||||||
|
type EditTurn = (instruction: string, text: string, opts?: RunEditTurnOptions) => Promise<EditTurnResult>;
|
||||||
/** F11: what an Ask-Claude gesture edits — a resolved selection range, or the whole document. */
|
/** F11: what an Ask-Claude gesture edits — a resolved selection range, or the whole document. */
|
||||||
type EditTarget = { kind: "range"; start: number; end: number } | { kind: "document" };
|
type EditTarget = { kind: "range"; start: number; end: number } | { kind: "document" };
|
||||||
|
|
||||||
@@ -53,9 +58,9 @@ export class TrackChangesPreviewController implements vscode.Disposable {
|
|||||||
* F11: the host edit turn (INV-8 — runs host-side, @cline/sdk loaded lazily and
|
* F11: the host edit turn (INV-8 — runs host-side, @cline/sdk loaded lazily and
|
||||||
* never bundled). Injectable so host E2E can stub it (no LLM in CI).
|
* never bundled). Injectable so host E2E can stub it (no LLM in CI).
|
||||||
*/
|
*/
|
||||||
private editTurn: EditTurn = async (instruction, text) => {
|
private editTurn: EditTurn = async (instruction, text, opts) => {
|
||||||
const { runEditTurn } = await import("./liveTurn");
|
const { runEditTurn } = await import("./liveTurn");
|
||||||
return runEditTurn(instruction, text);
|
return runEditTurn(instruction, text, opts);
|
||||||
};
|
};
|
||||||
/** Monotonic per-session counter minting a stable turnId for each Ask-Claude gesture. */
|
/** Monotonic per-session counter minting a stable turnId for each Ask-Claude gesture. */
|
||||||
private turnSeq = 0;
|
private turnSeq = 0;
|
||||||
@@ -68,6 +73,7 @@ export class TrackChangesPreviewController implements vscode.Disposable {
|
|||||||
private readonly extensionUri: vscode.Uri,
|
private readonly extensionUri: vscode.Uri,
|
||||||
private readonly attribution: AttributionController,
|
private readonly attribution: AttributionController,
|
||||||
private readonly proposals: ProposalController,
|
private readonly proposals: ProposalController,
|
||||||
|
private readonly liveProgressUi: LiveProgressUi,
|
||||||
) {
|
) {
|
||||||
this.disposables.push(
|
this.disposables.push(
|
||||||
// F11 (SLICE-5): the editor/title gateway passes the tab's resource Uri;
|
// F11 (SLICE-5): the editor/title gateway passes the tab's resource Uri;
|
||||||
@@ -230,8 +236,24 @@ export class TrackChangesPreviewController implements vscode.Disposable {
|
|||||||
if (!instruction) return;
|
if (!instruction) return;
|
||||||
try {
|
try {
|
||||||
const ids = await vscode.window.withProgress(
|
const ids = await vscode.window.withProgress(
|
||||||
{ location: vscode.ProgressLocation.Notification, title: "Cowriting: asking Claude…" },
|
{
|
||||||
() => this.runEditAndPropose(document, target, instruction),
|
location: vscode.ProgressLocation.Notification,
|
||||||
|
title: "Cowriting: asking Claude…",
|
||||||
|
cancellable: true,
|
||||||
|
},
|
||||||
|
async (progress, token) => {
|
||||||
|
const ui = this.liveProgressUi.begin(instruction, progress, token);
|
||||||
|
try {
|
||||||
|
return await this.runEditAndPropose(document, target, instruction, {
|
||||||
|
onProgress: ui.onProgress,
|
||||||
|
signal: ui.signal,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
// #60 (INV-47): a user cancel proposes nothing (the benign empty path).
|
||||||
|
if (token.isCancellationRequested) return [] as string[];
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
},
|
||||||
);
|
);
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
void vscode.window.showInformationMessage("Cowriting: Claude proposed no changes.");
|
void vscode.window.showInformationMessage("Cowriting: Claude proposed no changes.");
|
||||||
@@ -258,6 +280,7 @@ export class TrackChangesPreviewController implements vscode.Disposable {
|
|||||||
document: vscode.TextDocument,
|
document: vscode.TextDocument,
|
||||||
target: EditTarget,
|
target: EditTarget,
|
||||||
instruction: string,
|
instruction: string,
|
||||||
|
opts?: RunEditTurnOptions,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const full = document.getText();
|
const full = document.getText();
|
||||||
// One turnId per gesture — the document case's N hunk-proposals all share it,
|
// One turnId per gesture — the document case's N hunk-proposals all share it,
|
||||||
@@ -267,13 +290,13 @@ export class TrackChangesPreviewController implements vscode.Disposable {
|
|||||||
({ kind: "agent" as const, id: "claude", agent: { sdk: "@cline/sdk", model: turn.model, sessionId: turn.sessionId } });
|
({ kind: "agent" as const, id: "claude", agent: { sdk: "@cline/sdk", model: turn.model, sessionId: turn.sessionId } });
|
||||||
if (target.kind === "range") {
|
if (target.kind === "range") {
|
||||||
const selected = full.slice(target.start, target.end);
|
const selected = full.slice(target.start, target.end);
|
||||||
const turn = await this.editTurn(instruction, selected);
|
const turn = await this.editTurn(instruction, selected, opts);
|
||||||
if (turn.replacement === "" || turn.replacement === selected) return [];
|
if (turn.replacement === "" || turn.replacement === selected) return [];
|
||||||
const fp = buildFingerprint(full, { start: target.start, end: target.end });
|
const fp = buildFingerprint(full, { start: target.start, end: target.end });
|
||||||
const id = await this.proposals.propose(document, fp, turn.replacement, provenance(turn), { turnId, instruction });
|
const id = await this.proposals.propose(document, fp, turn.replacement, provenance(turn), { turnId, instruction });
|
||||||
return id ? [id] : [];
|
return id ? [id] : [];
|
||||||
}
|
}
|
||||||
const turn = await this.editTurn(instruction, full);
|
const turn = await this.editTurn(instruction, full, opts);
|
||||||
const ids: string[] = [];
|
const ids: string[] = [];
|
||||||
// #47 (INV-39, supersedes INV-37): a document rewrite is cut at BLOCK
|
// #47 (INV-39, supersedes INV-37): a document rewrite is cut at BLOCK
|
||||||
// granularity — one proposal per changed block (the unit a human reviews) —
|
// granularity — one proposal per changed block (the unit a human reviews) —
|
||||||
|
|||||||
Reference in New Issue
Block a user