From 2e402924e9d01ab6d5d2aa339d0436b1f2f250e6 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Mon, 12 Jan 2026 21:14:21 +0530 Subject: [PATCH 1/5] fix: prevent race conditions when applying multiple diffs in parallel --- gui/src/redux/selectors/selectToolCalls.ts | 8 ++++++++ gui/src/redux/thunks/handleApplyStateUpdate.ts | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/gui/src/redux/selectors/selectToolCalls.ts b/gui/src/redux/selectors/selectToolCalls.ts index a65f701404c..40bec1c73da 100644 --- a/gui/src/redux/selectors/selectToolCalls.ts +++ b/gui/src/redux/selectors/selectToolCalls.ts @@ -56,6 +56,14 @@ export const selectApplyStateByToolCallId = createSelector( }, ); +export const selectApplyStatesInProgress = createSelector( + (store: RootState) => store.session.codeBlockApplyStates.states, + (states) => + states.filter( + (state) => state.status === "not-started" || state.status === "streaming", + ), +); + // Status-specific convenience selectors export const selectPendingToolCalls = createSelector( (store: RootState) => store.session.history, diff --git a/gui/src/redux/thunks/handleApplyStateUpdate.ts b/gui/src/redux/thunks/handleApplyStateUpdate.ts index a220af51f63..0dd4e552c03 100644 --- a/gui/src/redux/thunks/handleApplyStateUpdate.ts +++ b/gui/src/redux/thunks/handleApplyStateUpdate.ts @@ -4,6 +4,7 @@ import { EDIT_MODE_STREAM_ID } from "core/edit/constants"; import { logAgentModeEditOutcome } from "../../util/editOutcomeLogger"; import { selectApplyStateByToolCallId, + selectApplyStatesInProgress, selectToolCallById, } from "../selectors/selectToolCalls"; import { updateEditStateApplyState } from "../slices/editState"; @@ -166,6 +167,16 @@ export const applyForEditTool = createAsyncThunk< }), ); + let isWaitingForPreviousApplyToFinish = true; + while (isWaitingForPreviousApplyToFinish) { + if (selectApplyStatesInProgress(getState()).length > 1) { + // wait for the previous apply state to finish because showing diffs on parallel in the ide has race conditions + await new Promise((resolve) => setTimeout(resolve, 1000)); + } else { + isWaitingForPreviousApplyToFinish = false; + } + } + let didError = false; try { const response = await extra.ideMessenger.request("applyToFile", payload); From 37a913350cd420dfcdcf5edfd28021957901a6d3 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Mon, 19 Jan 2026 19:15:03 +0530 Subject: [PATCH 2/5] change timeout to 100 ms --- gui/src/redux/thunks/handleApplyStateUpdate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/src/redux/thunks/handleApplyStateUpdate.ts b/gui/src/redux/thunks/handleApplyStateUpdate.ts index 0dd4e552c03..7d89f81cbd8 100644 --- a/gui/src/redux/thunks/handleApplyStateUpdate.ts +++ b/gui/src/redux/thunks/handleApplyStateUpdate.ts @@ -171,7 +171,7 @@ export const applyForEditTool = createAsyncThunk< while (isWaitingForPreviousApplyToFinish) { if (selectApplyStatesInProgress(getState()).length > 1) { // wait for the previous apply state to finish because showing diffs on parallel in the ide has race conditions - await new Promise((resolve) => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 100)); } else { isWaitingForPreviousApplyToFinish = false; } From fb2518138107dae13842438d32e04b4b22c37688 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:00:00 +0530 Subject: [PATCH 3/5] prevent relying on activeTextEditor for diff applys - for both reading and writing to the file when applying to it --- core/index.d.ts | 2 +- extensions/vscode/src/apply/ApplyManager.ts | 40 ++++++++----------- .../vscode/src/diff/vertical/manager.ts | 15 +++---- 3 files changed, 25 insertions(+), 32 deletions(-) diff --git a/core/index.d.ts b/core/index.d.ts index 6f23bfbdea2..9d9e8201e6c 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -1499,7 +1499,7 @@ export interface ShowFilePayload { export interface ApplyToFilePayload { streamId: string; - filepath?: string; + filepath: string; text: string; toolCallId?: string; isSearchAndReplace?: boolean; diff --git a/extensions/vscode/src/apply/ApplyManager.ts b/extensions/vscode/src/apply/ApplyManager.ts index de8344e1889..f40523b27ce 100644 --- a/extensions/vscode/src/apply/ApplyManager.ts +++ b/extensions/vscode/src/apply/ApplyManager.ts @@ -11,6 +11,7 @@ import { streamDiffLines } from "core/edit/streamDiffLines"; import { pruneLinesFromBottom, pruneLinesFromTop } from "core/llm/countTokens"; import { getMarkdownLanguageTagForFile } from "core/util"; import { VerticalDiffManager } from "../diff/vertical/manager"; +import { openEditorAndRevealRange } from "../util/vscode"; import { VsCodeIde } from "../VsCodeIde"; import { VsCodeWebviewProtocol } from "../webviewProtocol"; @@ -32,18 +33,14 @@ export class ApplyManager { toolCallId, isSearchAndReplace, }: ApplyToFilePayload) { - if (filepath) { - await this.ensureFileOpen(filepath); - } - - const { activeTextEditor } = vscode.window; - if (!activeTextEditor) { - void vscode.window.showErrorMessage("No active editor to apply edits to"); + const editor = await this.getOrCreateEditor(filepath); + if (!editor) { + void vscode.window.showErrorMessage("Failed to open editor for file"); return; } // Capture the original file content before applying changes - const originalFileContent = activeTextEditor.document.getText(); + const originalFileContent = editor.document.getText(); await this.webviewProtocol.request("updateApplyState", { streamId, @@ -53,42 +50,37 @@ export class ApplyManager { toolCallId, }); - const hasExistingDocument = !!activeTextEditor.document.getText().trim(); + const hasExistingDocument = !!editor.document.getText().trim(); if (hasExistingDocument) { // Currently `isSearchAndReplace` will always provide a full file rewrite // as the contents of `text`, so we can just instantly apply if (isSearchAndReplace) { await this.verticalDiffManager.instantApplyDiff( + filepath, originalFileContent, text, streamId, toolCallId, ); } else { - await this.handleExistingDocument( - activeTextEditor, - text, - streamId, - toolCallId, - ); + await this.handleExistingDocument(editor, text, streamId, toolCallId); } } else { - await this.handleEmptyDocument( - activeTextEditor, - text, - streamId, - toolCallId, - ); + await this.handleEmptyDocument(editor, text, streamId, toolCallId); } } - private async ensureFileOpen(filepath: string): Promise { + private async getOrCreateEditor( + filepath: string, + ): Promise { const fileExists = await this.ide.fileExists(filepath); if (!fileExists) { await this.ide.writeFile(filepath, ""); - await this.ide.openFile(filepath); } - await this.ide.openFile(filepath); + const uri = filepath.startsWith("file://") + ? vscode.Uri.parse(filepath) + : vscode.Uri.file(filepath); + return openEditorAndRevealRange(uri); } private modelIsTooFastForStreaming(model: string): boolean { diff --git a/extensions/vscode/src/diff/vertical/manager.ts b/extensions/vscode/src/diff/vertical/manager.ts index 059efe0aee4..4c5d9161a09 100644 --- a/extensions/vscode/src/diff/vertical/manager.ts +++ b/extensions/vscode/src/diff/vertical/manager.ts @@ -16,6 +16,7 @@ import { EDIT_MODE_STREAM_ID } from "core/edit/constants"; import { stripImages } from "core/util/messageContent"; import { getLastNPathParts } from "core/util/uri"; import { editOutcomeTracker } from "../../extension/EditOutcomeTracker"; +import { openEditorAndRevealRange } from "../../util/vscode"; import { VerticalDiffHandler, VerticalDiffHandlerOptions } from "./handler"; import { getFirstChangedLine } from "./util"; @@ -296,6 +297,7 @@ export class VerticalDiffManager { } async instantApplyDiff( + filepath: string, oldContent: string, newContent: string, streamId: string, @@ -303,17 +305,16 @@ export class VerticalDiffManager { ) { vscode.commands.executeCommand("setContext", "continue.diffVisible", true); - const editor = vscode.window.activeTextEditor; + const uri = vscode.Uri.parse(filepath); + const editor = await openEditorAndRevealRange(uri); if (!editor) { return; } - const fileUri = editor.document.uri.toString(); - const myersDiffs = myersDiff(oldContent, newContent); const diffHandler = this.createVerticalDiffHandler( - fileUri, + filepath, 0, editor.document.lineCount - 1, { @@ -324,7 +325,7 @@ export class VerticalDiffManager { status, numDiffs, fileContent, - filepath: fileUri, + filepath, toolCallId, }), streamId, @@ -347,9 +348,9 @@ export class VerticalDiffManager { await this.webviewProtocol.request("updateApplyState", { streamId, status: "done", - numDiffs: this.fileUriToCodeLens.get(fileUri)?.length ?? 0, + numDiffs: this.fileUriToCodeLens.get(filepath)?.length ?? 0, fileContent: editor.document.getText(), - filepath: fileUri, + filepath, toolCallId, }); } From 4d4211f8bde8d4a58392638c8e4a35e44115d6a5 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:00:40 +0530 Subject: [PATCH 4/5] remove selectApplyStatesInProgress --- gui/src/redux/selectors/selectToolCalls.ts | 8 -------- gui/src/redux/thunks/handleApplyStateUpdate.ts | 11 ----------- 2 files changed, 19 deletions(-) diff --git a/gui/src/redux/selectors/selectToolCalls.ts b/gui/src/redux/selectors/selectToolCalls.ts index 40bec1c73da..a65f701404c 100644 --- a/gui/src/redux/selectors/selectToolCalls.ts +++ b/gui/src/redux/selectors/selectToolCalls.ts @@ -56,14 +56,6 @@ export const selectApplyStateByToolCallId = createSelector( }, ); -export const selectApplyStatesInProgress = createSelector( - (store: RootState) => store.session.codeBlockApplyStates.states, - (states) => - states.filter( - (state) => state.status === "not-started" || state.status === "streaming", - ), -); - // Status-specific convenience selectors export const selectPendingToolCalls = createSelector( (store: RootState) => store.session.history, diff --git a/gui/src/redux/thunks/handleApplyStateUpdate.ts b/gui/src/redux/thunks/handleApplyStateUpdate.ts index 7d89f81cbd8..a220af51f63 100644 --- a/gui/src/redux/thunks/handleApplyStateUpdate.ts +++ b/gui/src/redux/thunks/handleApplyStateUpdate.ts @@ -4,7 +4,6 @@ import { EDIT_MODE_STREAM_ID } from "core/edit/constants"; import { logAgentModeEditOutcome } from "../../util/editOutcomeLogger"; import { selectApplyStateByToolCallId, - selectApplyStatesInProgress, selectToolCallById, } from "../selectors/selectToolCalls"; import { updateEditStateApplyState } from "../slices/editState"; @@ -167,16 +166,6 @@ export const applyForEditTool = createAsyncThunk< }), ); - let isWaitingForPreviousApplyToFinish = true; - while (isWaitingForPreviousApplyToFinish) { - if (selectApplyStatesInProgress(getState()).length > 1) { - // wait for the previous apply state to finish because showing diffs on parallel in the ide has race conditions - await new Promise((resolve) => setTimeout(resolve, 100)); - } else { - isWaitingForPreviousApplyToFinish = false; - } - } - let didError = false; try { const response = await extra.ideMessenger.request("applyToFile", payload); From 95c743d3646344e0c7555a32a8273076a9b3f997 Mon Sep 17 00:00:00 2001 From: uinstinct <61635505+uinstinct@users.noreply.github.com> Date: Wed, 4 Feb 2026 12:04:23 +0530 Subject: [PATCH 5/5] show pending status when applying diff in parallel --- .../redux/thunks/handleApplyStateUpdate.ts | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/gui/src/redux/thunks/handleApplyStateUpdate.ts b/gui/src/redux/thunks/handleApplyStateUpdate.ts index a220af51f63..7b4a82356a2 100644 --- a/gui/src/redux/thunks/handleApplyStateUpdate.ts +++ b/gui/src/redux/thunks/handleApplyStateUpdate.ts @@ -10,6 +10,7 @@ import { updateEditStateApplyState } from "../slices/editState"; import { acceptToolCall, errorToolCall, + setToolGenerated, updateApplyState, updateToolCallOutput, } from "../slices/sessionSlice"; @@ -49,16 +50,22 @@ export const handleApplyStateUpdate = createAsyncThunk< applyState.toolCallId, ); - if ( - applyState.status === "done" && - toolCallState?.toolCall.function.name && - getState().ui.toolSettings[toolCallState.toolCall.function.name] === - "allowedWithoutPermission" - ) { - extra.ideMessenger.post("acceptDiff", { - streamId: applyState.streamId, - filepath: applyState.filepath, - }); + if (applyState.status === "done") { + // diff is ready for user accept/reject - set status back to "generated" + dispatch( + setToolGenerated({ toolCallId: applyState.toolCallId, tools: [] }), + ); + + if ( + toolCallState?.toolCall.function.name && + getState().ui.toolSettings[toolCallState.toolCall.function.name] === + "allowedWithoutPermission" + ) { + extra.ideMessenger.post("acceptDiff", { + streamId: applyState.streamId, + filepath: applyState.filepath, + }); + } } if (applyState.status === "closed") {