Skip to content

Commit 9393064

Browse files
roomote[bot]ellipsis-dev[bot]roomotedaniel-lxs
authored
feat: Add experimental setting to prevent editor focus disruption (RooCodeInc#6214)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: Roo Code <[email protected]> Co-authored-by: Daniel Riccio <[email protected]> Co-authored-by: Daniel <[email protected]>
1 parent 8b9303c commit 9393064

30 files changed

+612
-154
lines changed

packages/types/src/experiment.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js"
66
* ExperimentId
77
*/
88

9-
export const experimentIds = ["powerSteering", "multiFileApplyDiff"] as const
9+
export const experimentIds = ["powerSteering", "multiFileApplyDiff", "preventFocusDisruption"] as const
1010

1111
export const experimentIdsSchema = z.enum(experimentIds)
1212

@@ -19,6 +19,7 @@ export type ExperimentId = z.infer<typeof experimentIdsSchema>
1919
export const experimentsSchema = z.object({
2020
powerSteering: z.boolean().optional(),
2121
multiFileApplyDiff: z.boolean().optional(),
22+
preventFocusDisruption: z.boolean().optional(),
2223
})
2324

2425
export type Experiments = z.infer<typeof experimentsSchema>

src/core/tools/applyDiffTool.ts

Lines changed: 66 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { formatResponse } from "../prompts/responses"
1212
import { fileExistsAtPath } from "../../utils/fs"
1313
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
1414
import { unescapeHtmlEntities } from "../../utils/text-normalization"
15+
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
1516

1617
export async function applyDiffToolLegacy(
1718
cline: Task,
@@ -87,7 +88,7 @@ export async function applyDiffToolLegacy(
8788
return
8889
}
8990

90-
let originalContent: string | null = await fs.readFile(absolutePath, "utf-8")
91+
const originalContent: string = await fs.readFile(absolutePath, "utf-8")
9192

9293
// Apply the diff to the original content
9394
const diffResult = (await cline.diffStrategy?.applyDiff(
@@ -99,9 +100,6 @@ export async function applyDiffToolLegacy(
99100
error: "No diff strategy available",
100101
}
101102

102-
// Release the original content from memory as it's no longer needed
103-
originalContent = null
104-
105103
if (!diffResult.success) {
106104
cline.consecutiveMistakeCount++
107105
const currentCount = (cline.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1
@@ -142,40 +140,79 @@ export async function applyDiffToolLegacy(
142140
cline.consecutiveMistakeCount = 0
143141
cline.consecutiveMistakeCountForApplyDiff.delete(relPath)
144142

145-
// Show diff view before asking for approval
146-
cline.diffViewProvider.editType = "modify"
147-
await cline.diffViewProvider.open(relPath)
148-
await cline.diffViewProvider.update(diffResult.content, true)
149-
cline.diffViewProvider.scrollToFirstDiff()
143+
// Check if preventFocusDisruption experiment is enabled
144+
const provider = cline.providerRef.deref()
145+
const state = await provider?.getState()
146+
const diagnosticsEnabled = state?.diagnosticsEnabled ?? true
147+
const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS
148+
const isPreventFocusDisruptionEnabled = experiments.isEnabled(
149+
state?.experiments ?? {},
150+
EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION,
151+
)
150152

151153
// Check if file is write-protected
152154
const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
153155

154-
const completeMessage = JSON.stringify({
155-
...sharedMessageProps,
156-
diff: diffContent,
157-
isProtected: isWriteProtected,
158-
} satisfies ClineSayTool)
156+
if (isPreventFocusDisruptionEnabled) {
157+
// Direct file write without diff view
158+
const completeMessage = JSON.stringify({
159+
...sharedMessageProps,
160+
diff: diffContent,
161+
isProtected: isWriteProtected,
162+
} satisfies ClineSayTool)
159163

160-
let toolProgressStatus
164+
let toolProgressStatus
161165

162-
if (cline.diffStrategy && cline.diffStrategy.getProgressStatus) {
163-
toolProgressStatus = cline.diffStrategy.getProgressStatus(block, diffResult)
164-
}
166+
if (cline.diffStrategy && cline.diffStrategy.getProgressStatus) {
167+
toolProgressStatus = cline.diffStrategy.getProgressStatus(block, diffResult)
168+
}
165169

166-
const didApprove = await askApproval("tool", completeMessage, toolProgressStatus, isWriteProtected)
170+
const didApprove = await askApproval("tool", completeMessage, toolProgressStatus, isWriteProtected)
167171

168-
if (!didApprove) {
169-
await cline.diffViewProvider.revertChanges() // Cline likely handles closing the diff view
170-
return
171-
}
172+
if (!didApprove) {
173+
return
174+
}
172175

173-
// Call saveChanges to update the DiffViewProvider properties
174-
const provider = cline.providerRef.deref()
175-
const state = await provider?.getState()
176-
const diagnosticsEnabled = state?.diagnosticsEnabled ?? true
177-
const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS
178-
await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
176+
// Save directly without showing diff view or opening the file
177+
cline.diffViewProvider.editType = "modify"
178+
cline.diffViewProvider.originalContent = originalContent
179+
await cline.diffViewProvider.saveDirectly(
180+
relPath,
181+
diffResult.content,
182+
false,
183+
diagnosticsEnabled,
184+
writeDelayMs,
185+
)
186+
} else {
187+
// Original behavior with diff view
188+
// Show diff view before asking for approval
189+
cline.diffViewProvider.editType = "modify"
190+
await cline.diffViewProvider.open(relPath)
191+
await cline.diffViewProvider.update(diffResult.content, true)
192+
cline.diffViewProvider.scrollToFirstDiff()
193+
194+
const completeMessage = JSON.stringify({
195+
...sharedMessageProps,
196+
diff: diffContent,
197+
isProtected: isWriteProtected,
198+
} satisfies ClineSayTool)
199+
200+
let toolProgressStatus
201+
202+
if (cline.diffStrategy && cline.diffStrategy.getProgressStatus) {
203+
toolProgressStatus = cline.diffStrategy.getProgressStatus(block, diffResult)
204+
}
205+
206+
const didApprove = await askApproval("tool", completeMessage, toolProgressStatus, isWriteProtected)
207+
208+
if (!didApprove) {
209+
await cline.diffViewProvider.revertChanges() // Cline likely handles closing the diff view
210+
return
211+
}
212+
213+
// Call saveChanges to update the DiffViewProvider properties
214+
await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
215+
}
179216

180217
// Track file edit operation
181218
if (relPath) {

src/core/tools/insertContentTool.ts

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
1111
import { fileExistsAtPath } from "../../utils/fs"
1212
import { insertGroups } from "../diff/insert-groups"
1313
import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
14+
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
1415

1516
export async function insertContentTool(
1617
cline: Task,
@@ -107,15 +108,15 @@ export async function insertContentTool(
107108
},
108109
]).join("\n")
109110

110-
// Show changes in diff view
111-
if (!cline.diffViewProvider.isEditing) {
112-
await cline.ask("tool", JSON.stringify(sharedMessageProps), true).catch(() => {})
113-
// First open with original content
114-
await cline.diffViewProvider.open(relPath)
115-
await cline.diffViewProvider.update(fileContent, false)
116-
cline.diffViewProvider.scrollToFirstDiff()
117-
await delay(200)
118-
}
111+
// Check if preventFocusDisruption experiment is enabled
112+
const provider = cline.providerRef.deref()
113+
const state = await provider?.getState()
114+
const diagnosticsEnabled = state?.diagnosticsEnabled ?? true
115+
const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS
116+
const isPreventFocusDisruptionEnabled = experiments.isEnabled(
117+
state?.experiments ?? {},
118+
EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION,
119+
)
119120

120121
// For consistency with writeToFileTool, handle new files differently
121122
let diff: string | undefined
@@ -135,8 +136,6 @@ export async function insertContentTool(
135136
approvalContent = updatedContent
136137
}
137138

138-
await cline.diffViewProvider.update(updatedContent, true)
139-
140139
const completeMessage = JSON.stringify({
141140
...sharedMessageProps,
142141
diff,
@@ -150,17 +149,33 @@ export async function insertContentTool(
150149
.then((response) => response.response === "yesButtonClicked")
151150

152151
if (!didApprove) {
153-
await cline.diffViewProvider.revertChanges()
152+
if (!isPreventFocusDisruptionEnabled) {
153+
await cline.diffViewProvider.revertChanges()
154+
}
154155
pushToolResult("Changes were rejected by the user.")
155156
return
156157
}
157158

158-
// Call saveChanges to update the DiffViewProvider properties
159-
const provider = cline.providerRef.deref()
160-
const state = await provider?.getState()
161-
const diagnosticsEnabled = state?.diagnosticsEnabled ?? true
162-
const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS
163-
await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
159+
if (isPreventFocusDisruptionEnabled) {
160+
// Direct file write without diff view or opening the file
161+
await cline.diffViewProvider.saveDirectly(relPath, updatedContent, false, diagnosticsEnabled, writeDelayMs)
162+
} else {
163+
// Original behavior with diff view
164+
// Show changes in diff view
165+
if (!cline.diffViewProvider.isEditing) {
166+
await cline.ask("tool", JSON.stringify(sharedMessageProps), true).catch(() => {})
167+
// First open with original content
168+
await cline.diffViewProvider.open(relPath)
169+
await cline.diffViewProvider.update(fileContent, false)
170+
cline.diffViewProvider.scrollToFirstDiff()
171+
await delay(200)
172+
}
173+
174+
await cline.diffViewProvider.update(updatedContent, true)
175+
176+
// Call saveChanges to update the DiffViewProvider properties
177+
await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
178+
}
164179

165180
// Track file edit operation
166181
if (relPath) {

src/core/tools/multiApplyDiffTool.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -507,11 +507,15 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""}
507507
cline.consecutiveMistakeCount = 0
508508
cline.consecutiveMistakeCountForApplyDiff.delete(relPath)
509509

510-
// Show diff view before asking for approval (only for single file or after batch approval)
511-
cline.diffViewProvider.editType = "modify"
512-
await cline.diffViewProvider.open(relPath)
513-
await cline.diffViewProvider.update(originalContent!, true)
514-
cline.diffViewProvider.scrollToFirstDiff()
510+
// Check if preventFocusDisruption experiment is enabled
511+
const provider = cline.providerRef.deref()
512+
const state = await provider?.getState()
513+
const diagnosticsEnabled = state?.diagnosticsEnabled ?? true
514+
const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS
515+
const isPreventFocusDisruptionEnabled = experiments.isEnabled(
516+
state?.experiments ?? {},
517+
EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION,
518+
)
515519

516520
// For batch operations, we've already gotten approval
517521
const isWriteProtected = cline.rooProtectedController?.isWriteProtected(relPath) || false
@@ -548,17 +552,35 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""}
548552
}
549553

550554
if (!didApprove) {
551-
await cline.diffViewProvider.revertChanges()
555+
if (!isPreventFocusDisruptionEnabled) {
556+
await cline.diffViewProvider.revertChanges()
557+
}
552558
results.push(`Changes to ${relPath} were not approved by user`)
553559
continue
554560
}
555561

556-
// Call saveChanges to update the DiffViewProvider properties
557-
const provider = cline.providerRef.deref()
558-
const state = await provider?.getState()
559-
const diagnosticsEnabled = state?.diagnosticsEnabled ?? true
560-
const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS
561-
await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
562+
if (isPreventFocusDisruptionEnabled) {
563+
// Direct file write without diff view or opening the file
564+
cline.diffViewProvider.editType = "modify"
565+
cline.diffViewProvider.originalContent = await fs.readFile(absolutePath, "utf-8")
566+
await cline.diffViewProvider.saveDirectly(
567+
relPath,
568+
originalContent!,
569+
false,
570+
diagnosticsEnabled,
571+
writeDelayMs,
572+
)
573+
} else {
574+
// Original behavior with diff view
575+
// Show diff view before asking for approval (only for single file or after batch approval)
576+
cline.diffViewProvider.editType = "modify"
577+
await cline.diffViewProvider.open(relPath)
578+
await cline.diffViewProvider.update(originalContent!, true)
579+
cline.diffViewProvider.scrollToFirstDiff()
580+
581+
// Call saveChanges to update the DiffViewProvider properties
582+
await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
583+
}
562584

563585
// Track file edit operation
564586
await cline.fileContextTracker.trackFileContext(relPath, "roo_edited" as RecordSource)

src/core/tools/searchAndReplaceTool.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { getReadablePath } from "../../utils/path"
1212
import { fileExistsAtPath } from "../../utils/fs"
1313
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
1414
import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
15+
import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
1516

1617
/**
1718
* Tool for performing search and replace operations on files
@@ -199,16 +200,15 @@ export async function searchAndReplaceTool(
199200
return
200201
}
201202

202-
// Show changes in diff view
203-
if (!cline.diffViewProvider.isEditing) {
204-
await cline.ask("tool", JSON.stringify(sharedMessageProps), true).catch(() => {})
205-
await cline.diffViewProvider.open(validRelPath)
206-
await cline.diffViewProvider.update(fileContent, false)
207-
cline.diffViewProvider.scrollToFirstDiff()
208-
await delay(200)
209-
}
210-
211-
await cline.diffViewProvider.update(newContent, true)
203+
// Check if preventFocusDisruption experiment is enabled
204+
const provider = cline.providerRef.deref()
205+
const state = await provider?.getState()
206+
const diagnosticsEnabled = state?.diagnosticsEnabled ?? true
207+
const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS
208+
const isPreventFocusDisruptionEnabled = experiments.isEnabled(
209+
state?.experiments ?? {},
210+
EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION,
211+
)
212212

213213
// Request user approval for changes
214214
const completeMessage = JSON.stringify({
@@ -221,18 +221,33 @@ export async function searchAndReplaceTool(
221221
.then((response) => response.response === "yesButtonClicked")
222222

223223
if (!didApprove) {
224-
await cline.diffViewProvider.revertChanges()
224+
if (!isPreventFocusDisruptionEnabled) {
225+
await cline.diffViewProvider.revertChanges()
226+
}
225227
pushToolResult("Changes were rejected by the user.")
226228
await cline.diffViewProvider.reset()
227229
return
228230
}
229231

230-
// Call saveChanges to update the DiffViewProvider properties
231-
const provider = cline.providerRef.deref()
232-
const state = await provider?.getState()
233-
const diagnosticsEnabled = state?.diagnosticsEnabled ?? true
234-
const writeDelayMs = state?.writeDelayMs ?? DEFAULT_WRITE_DELAY_MS
235-
await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
232+
if (isPreventFocusDisruptionEnabled) {
233+
// Direct file write without diff view or opening the file
234+
await cline.diffViewProvider.saveDirectly(validRelPath, newContent, false, diagnosticsEnabled, writeDelayMs)
235+
} else {
236+
// Original behavior with diff view
237+
// Show changes in diff view
238+
if (!cline.diffViewProvider.isEditing) {
239+
await cline.ask("tool", JSON.stringify(sharedMessageProps), true).catch(() => {})
240+
await cline.diffViewProvider.open(validRelPath)
241+
await cline.diffViewProvider.update(fileContent, false)
242+
cline.diffViewProvider.scrollToFirstDiff()
243+
await delay(200)
244+
}
245+
246+
await cline.diffViewProvider.update(newContent, true)
247+
248+
// Call saveChanges to update the DiffViewProvider properties
249+
await cline.diffViewProvider.saveChanges(diagnosticsEnabled, writeDelayMs)
250+
}
236251

237252
// Track file edit operation
238253
if (relPath) {

0 commit comments

Comments
 (0)