Merge PR #6444: fix(plan): preserve and line-anchor review annotations (@anatoli-tsinovoy)
This commit is contained in:
@@ -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.");
|
||||
|
||||
Reference in New Issue
Block a user