Merge PR #6444: fix(plan): preserve and line-anchor review annotations (@anatoli-tsinovoy)

This commit is contained in:
can1357
2026-07-24 02:24:05 +02:00
5 changed files with 784 additions and 48 deletions
@@ -153,7 +153,7 @@ import type { EvalExecutionComponent } from "./components/eval-execution";
import type { HookEditorComponent } from "./components/hook-editor";
import type { HookInputComponent } from "./components/hook-input";
import type { HookSelectorComponent, HookSelectorSlider } from "./components/hook-selector";
import { PlanReviewOverlay } from "./components/plan-review-overlay";
import { type PlanReviewAnnotationState, PlanReviewOverlay } from "./components/plan-review-overlay";
import { StatusLineComponent } from "./components/status-line";
import type { ToolExecutionHandle } from "./components/tool-execution";
import { TranscriptContainer } from "./components/transcript-container";
@@ -569,6 +569,10 @@ export class InteractiveMode implements InteractiveModeContext {
#planReviewOverlay: PlanReviewOverlay | undefined;
#planReviewOverlayHandle: OverlayHandle | undefined;
#planReviewCancel: (() => void) | undefined;
/** Serializable review annotations keyed by the resolved plan file path. */
#planReviewAnnotationState = new Map<string, PlanReviewAnnotationState>();
/** Annotation state held until the associated queued refinement actually starts. */
#planReviewAnnotationStateBySubmission = new WeakMap<SubmittedUserInput, string>();
readonly lspServers: LspStartupServerInfo[] | undefined = undefined;
mcpManager?: MCPManager;
readonly #toolUiContextSetter: (uiContext: ExtensionUIContext, hasUI: boolean) => void;
@@ -1587,6 +1591,11 @@ export class InteractiveMode implements InteractiveModeContext {
return false;
}
input.started = true;
const annotationStateKey = this.#planReviewAnnotationStateBySubmission.get(input);
if (annotationStateKey) {
this.#planReviewAnnotationStateBySubmission.delete(input);
this.#planReviewAnnotationState.delete(annotationStateKey);
}
return true;
}
@@ -2719,6 +2728,8 @@ export class InteractiveMode implements InteractiveModeContext {
onExternalEditor?: () => void;
onPlanEdited?: (content: string) => void;
onFeedbackChange?: (feedback: string) => void;
annotationState?: PlanReviewAnnotationState;
onAnnotationStateChange?: (state: PlanReviewAnnotationState) => void;
initialIndex?: number;
},
extra?: { slider?: HookSelectorSlider },
@@ -2742,6 +2753,7 @@ export class InteractiveMode implements InteractiveModeContext {
initialIndex: dialogOptions?.initialIndex,
slider: extra?.slider,
externalEditorLabel: this.keybindings.getDisplayString("app.editor.external") || undefined,
annotationState: dialogOptions?.annotationState,
},
{
onPick: choice => finish(choice),
@@ -2751,6 +2763,7 @@ export class InteractiveMode implements InteractiveModeContext {
onAnnotationExternalEditor: (draft, commit) => void this.#openPlanAnnotationInExternalEditor(draft, commit),
onPlanEdited: dialogOptions?.onPlanEdited,
onFeedbackChange: dialogOptions?.onFeedbackChange,
onAnnotationStateChange: dialogOptions?.onAnnotationStateChange,
},
);
this.#planReviewOverlay = overlay;
@@ -2980,7 +2993,7 @@ export class InteractiveMode implements InteractiveModeContext {
compactBeforeExecute?: boolean;
executionModel?: ResolvedRoleModel;
},
): Promise<void> {
): Promise<boolean> {
const previousTools = this.#planModePreviousTools ?? this.session.getEnabledToolNames();
// Mark the pending abort caused by the plan-mode → compaction transition as
@@ -3079,7 +3092,7 @@ export class InteractiveMode implements InteractiveModeContext {
this.showWarning(
"Plan approved, but compaction was cancelled — execution not dispatched. Submit a turn to continue.",
);
return;
return false;
}
// Approved plans land in a fresh (or compacted) session whose first user-visible
@@ -3121,14 +3134,15 @@ export class InteractiveMode implements InteractiveModeContext {
// noted below), catch `AgentBusyError` and fall back to the same queue.
if (this.session.isStreaming) {
await this.session.followUp(planModePrompt, undefined, { synthetic: true });
return;
}
try {
await this.session.prompt(planModePrompt, { synthetic: true });
} catch (error) {
if (!(error instanceof AgentBusyError)) throw error;
await this.session.followUp(planModePrompt, undefined, { synthetic: true });
} else {
try {
await this.session.prompt(planModePrompt, { synthetic: true });
} catch (error) {
if (!(error instanceof AgentBusyError)) throw error;
await this.session.followUp(planModePrompt, undefined, { synthetic: true });
}
}
return true;
}
async #abortPlanApprovalTurnSilently(): Promise<void> {
this.session.markPlanInternalAbortPending();
@@ -3657,6 +3671,7 @@ export class InteractiveMode implements InteractiveModeContext {
// that the Refine branch re-prompts the model with.
let editedContent: string | undefined;
let feedback = "";
const annotationStateKey = this.#resolvePlanFilePath(planFilePath);
const choice = await this.showPlanReview(
planContent,
@@ -3672,6 +3687,11 @@ export class InteractiveMode implements InteractiveModeContext {
onFeedbackChange: value => {
feedback = value;
},
annotationState: this.#planReviewAnnotationState.get(annotationStateKey),
onAnnotationStateChange: state => {
if (state.annotations.length > 0) this.#planReviewAnnotationState.set(annotationStateKey, state);
else this.#planReviewAnnotationState.delete(annotationStateKey);
},
disabledIndices: keepContextDisabled ? [PLAN_KEEP_CONTEXT_OPTION_INDEX] : undefined,
},
{ slider },
@@ -3723,13 +3743,14 @@ export class InteractiveMode implements InteractiveModeContext {
: -1;
const executionModel =
slider && cycle && selectedTierIndex !== restoredIndex ? cycle.models[selectedTierIndex] : undefined;
await this.#approvePlan(latestPlanContent, {
const executionDispatched = await this.#approvePlan(latestPlanContent, {
planFilePath,
title: details.title,
preserveContext: choice !== "Approve and execute",
compactBeforeExecute: choice === "Approve and compact context",
executionModel,
});
if (executionDispatched) this.#planReviewAnnotationState.delete(annotationStateKey);
} catch (error) {
this.showError(
`Failed to finalize approved plan: ${error instanceof Error ? error.message : String(error)}`,
@@ -3744,9 +3765,12 @@ export class InteractiveMode implements InteractiveModeContext {
try {
if (refinement) {
if (this.onInputCallback) {
this.onInputCallback(this.startPendingSubmission({ text: feedback }));
const input = this.startPendingSubmission({ text: feedback });
this.#planReviewAnnotationStateBySubmission.set(input, annotationStateKey);
this.onInputCallback(input);
} else {
await this.session.prompt(feedback);
this.#planReviewAnnotationState.delete(annotationStateKey);
}
} else {
this.showStatus("Refine plan: enter a follow-up prompt.");