diff --git a/packages/types/src/experiment.ts b/packages/types/src/experiment.ts index 37c6eecee756..5df46a09f098 100644 --- a/packages/types/src/experiment.ts +++ b/packages/types/src/experiment.ts @@ -12,6 +12,8 @@ export const experimentIds = [ "preventFocusDisruption", "imageGeneration", "runSlashCommand", + "reReadAfterEdit", + "reReadAfterEditGranular", ] as const export const experimentIdsSchema = z.enum(experimentIds) @@ -22,12 +24,25 @@ export type ExperimentId = z.infer * Experiments */ +// Schema for granular re-read after edit settings +export const reReadAfterEditGranularSchema = z.object({ + applyDiff: z.boolean().optional(), + multiApplyDiff: z.boolean().optional(), + writeToFile: z.boolean().optional(), + insertContent: z.boolean().optional(), + searchAndReplace: z.boolean().optional(), +}) + +export type ReReadAfterEditGranular = z.infer + export const experimentsSchema = z.object({ powerSteering: z.boolean().optional(), multiFileApplyDiff: z.boolean().optional(), preventFocusDisruption: z.boolean().optional(), imageGeneration: z.boolean().optional(), runSlashCommand: z.boolean().optional(), + reReadAfterEdit: z.boolean().optional(), + reReadAfterEditGranular: reReadAfterEditGranularSchema.optional(), }) export type Experiments = z.infer diff --git a/src/core/tools/applyDiffTool.ts b/src/core/tools/applyDiffTool.ts index dcdd13462401..29a79f8251f6 100644 --- a/src/core/tools/applyDiffTool.ts +++ b/src/core/tools/applyDiffTool.ts @@ -238,10 +238,17 @@ export async function applyDiffToolLegacy( ? "\nMaking multiple related changes in a single apply_diff is more efficient. If other changes are needed in this file, please include them as additional SEARCH/REPLACE blocks." : "" + // Check if RE_READ_AFTER_EDIT experiment is enabled for applyDiff + const isReReadAfterEditEnabled = experiments.isReReadAfterEditEnabled(state?.experiments ?? {}, "applyDiff") + + const reReadSuggestion = isReReadAfterEditEnabled + ? `\n\nThe file has been edited. Consider using the read_file tool to review the changes and ensure they are correct and complete.` + : "" + if (partFailHint) { - pushToolResult(partFailHint + message + singleBlockNotice) + pushToolResult(partFailHint + message + singleBlockNotice + reReadSuggestion) } else { - pushToolResult(message + singleBlockNotice) + pushToolResult(message + singleBlockNotice + reReadSuggestion) } await cline.diffViewProvider.reset() diff --git a/src/core/tools/insertContentTool.ts b/src/core/tools/insertContentTool.ts index e7d3a06ab92d..a74ee7edb91a 100644 --- a/src/core/tools/insertContentTool.ts +++ b/src/core/tools/insertContentTool.ts @@ -184,7 +184,14 @@ export async function insertContentTool( // Get the formatted response message const message = await cline.diffViewProvider.pushToolWriteResult(cline, cline.cwd, !fileExists) - pushToolResult(message) + // Check if RE_READ_AFTER_EDIT experiment is enabled for insertContent + const isReReadAfterEditEnabled = experiments.isReReadAfterEditEnabled(state?.experiments ?? {}, "insertContent") + + const reReadSuggestion = isReReadAfterEditEnabled + ? `\n\nContent has been inserted into the file. Consider using the read_file tool to review the changes and ensure they are correct and complete.` + : "" + + pushToolResult(message + reReadSuggestion) await cline.diffViewProvider.reset() diff --git a/src/core/tools/multiApplyDiffTool.ts b/src/core/tools/multiApplyDiffTool.ts index a30778c5af0d..3ea36337e5ad 100644 --- a/src/core/tools/multiApplyDiffTool.ts +++ b/src/core/tools/multiApplyDiffTool.ts @@ -676,8 +676,23 @@ ${errorDetails ? `\nTechnical details:\n${errorDetails}\n` : ""} ? "\nMaking multiple related changes in a single apply_diff is more efficient. If other changes are needed in this file, please include them as additional SEARCH/REPLACE blocks." : "" + // Check if RE_READ_AFTER_EDIT experiment is enabled for multiApplyDiff + const provider = cline.providerRef.deref() + const state = await provider?.getState() + const isReReadAfterEditEnabled = experiments.isReReadAfterEditEnabled( + state?.experiments ?? {}, + "multiApplyDiff", + ) + + // Count how many files were successfully edited + const editedFiles = operationResults.filter((op) => op.status === "approved").length + const reReadSuggestion = + isReReadAfterEditEnabled && editedFiles > 0 + ? `\n\n${editedFiles === 1 ? "The file has" : `${editedFiles} files have`} been edited. Consider using the read_file tool to review the changes and ensure they are correct and complete.` + : "" + // Push the final result combining all operation results - pushToolResult(results.join("\n\n") + singleBlockNotice) + pushToolResult(results.join("\n\n") + singleBlockNotice + reReadSuggestion) cline.processQueuedMessages() return } catch (error) { diff --git a/src/core/tools/searchAndReplaceTool.ts b/src/core/tools/searchAndReplaceTool.ts index b0ee3947e1eb..f513b49c7b14 100644 --- a/src/core/tools/searchAndReplaceTool.ts +++ b/src/core/tools/searchAndReplaceTool.ts @@ -258,7 +258,17 @@ export async function searchAndReplaceTool( false, // Always false for search_and_replace ) - pushToolResult(message) + // Check if RE_READ_AFTER_EDIT experiment is enabled for searchAndReplace + const isReReadAfterEditEnabled = experiments.isReReadAfterEditEnabled( + state?.experiments ?? {}, + "searchAndReplace", + ) + + const reReadSuggestion = isReReadAfterEditEnabled + ? `\n\nThe file has been modified via search and replace. Consider using the read_file tool to review the changes and ensure they are correct and complete.` + : "" + + pushToolResult(message + reReadSuggestion) // Record successful tool usage and cleanup cline.recordToolUsage("search_and_replace") diff --git a/src/core/tools/writeToFileTool.ts b/src/core/tools/writeToFileTool.ts index 5abd96a20aff..f0bc08e40751 100644 --- a/src/core/tools/writeToFileTool.ts +++ b/src/core/tools/writeToFileTool.ts @@ -304,7 +304,17 @@ export async function writeToFileTool( // Get the formatted response message const message = await cline.diffViewProvider.pushToolWriteResult(cline, cline.cwd, !fileExists) - pushToolResult(message) + // Check if RE_READ_AFTER_EDIT experiment is enabled for writeToFile + const isReReadAfterEditEnabled = experiments.isReReadAfterEditEnabled( + state?.experiments ?? {}, + "writeToFile", + ) + + const reReadSuggestion = isReReadAfterEditEnabled + ? `\n\nThe file has been ${fileExists ? "edited" : "created"}. Consider using the read_file tool to review the ${fileExists ? "changes" : "content"} and ensure ${fileExists ? "they are" : "it is"} correct and complete.` + : "" + + pushToolResult(message + reReadSuggestion) await cline.diffViewProvider.reset() diff --git a/src/shared/__tests__/experiments-granular.spec.ts b/src/shared/__tests__/experiments-granular.spec.ts new file mode 100644 index 000000000000..0f3ee0b4eae9 --- /dev/null +++ b/src/shared/__tests__/experiments-granular.spec.ts @@ -0,0 +1,96 @@ +import { describe, it, expect } from "vitest" +import type { Experiments, ReReadAfterEditGranular } from "@roo-code/types" +import { experiments } from "../experiments" + +describe("granular re-read after edit experiment", () => { + describe("isReReadAfterEditEnabled", () => { + it("should return true when legacy reReadAfterEdit is enabled", () => { + const config: Experiments = { + reReadAfterEdit: true, + } + + expect(experiments.isReReadAfterEditEnabled(config, "applyDiff")).toBe(true) + expect(experiments.isReReadAfterEditEnabled(config, "multiApplyDiff")).toBe(true) + expect(experiments.isReReadAfterEditEnabled(config, "writeToFile")).toBe(true) + expect(experiments.isReReadAfterEditEnabled(config, "insertContent")).toBe(true) + expect(experiments.isReReadAfterEditEnabled(config, "searchAndReplace")).toBe(true) + }) + + it("should return false when legacy reReadAfterEdit is disabled and no granular settings", () => { + const config: Experiments = { + reReadAfterEdit: false, + } + + expect(experiments.isReReadAfterEditEnabled(config, "applyDiff")).toBe(false) + expect(experiments.isReReadAfterEditEnabled(config, "multiApplyDiff")).toBe(false) + expect(experiments.isReReadAfterEditEnabled(config, "writeToFile")).toBe(false) + expect(experiments.isReReadAfterEditEnabled(config, "insertContent")).toBe(false) + expect(experiments.isReReadAfterEditEnabled(config, "searchAndReplace")).toBe(false) + }) + + it("should return true for specific edit types when granular settings are enabled", () => { + const config: Experiments = { + reReadAfterEdit: false, + reReadAfterEditGranular: { + applyDiff: true, + multiApplyDiff: false, + writeToFile: true, + insertContent: false, + searchAndReplace: true, + }, + } + + expect(experiments.isReReadAfterEditEnabled(config, "applyDiff")).toBe(true) + expect(experiments.isReReadAfterEditEnabled(config, "multiApplyDiff")).toBe(false) + expect(experiments.isReReadAfterEditEnabled(config, "writeToFile")).toBe(true) + expect(experiments.isReReadAfterEditEnabled(config, "insertContent")).toBe(false) + expect(experiments.isReReadAfterEditEnabled(config, "searchAndReplace")).toBe(true) + }) + + it("should prioritize legacy setting over granular when both are present", () => { + const config: Experiments = { + reReadAfterEdit: true, // Legacy enabled + reReadAfterEditGranular: { + applyDiff: false, // Granular disabled + multiApplyDiff: false, + writeToFile: false, + insertContent: false, + searchAndReplace: false, + }, + } + + // Legacy setting should override granular settings + expect(experiments.isReReadAfterEditEnabled(config, "applyDiff")).toBe(true) + expect(experiments.isReReadAfterEditEnabled(config, "multiApplyDiff")).toBe(true) + expect(experiments.isReReadAfterEditEnabled(config, "writeToFile")).toBe(true) + expect(experiments.isReReadAfterEditEnabled(config, "insertContent")).toBe(true) + expect(experiments.isReReadAfterEditEnabled(config, "searchAndReplace")).toBe(true) + }) + + it("should handle partial granular settings", () => { + const config: Experiments = { + reReadAfterEdit: false, + reReadAfterEditGranular: { + applyDiff: true, + // Other fields undefined + } as ReReadAfterEditGranular, + } + + expect(experiments.isReReadAfterEditEnabled(config, "applyDiff")).toBe(true) + expect(experiments.isReReadAfterEditEnabled(config, "multiApplyDiff")).toBe(false) + expect(experiments.isReReadAfterEditEnabled(config, "writeToFile")).toBe(false) + expect(experiments.isReReadAfterEditEnabled(config, "insertContent")).toBe(false) + expect(experiments.isReReadAfterEditEnabled(config, "searchAndReplace")).toBe(false) + }) + + it("should return false when no experiments config is provided", () => { + const config: Experiments = {} + + expect(experiments.isReReadAfterEditEnabled(config, "applyDiff")).toBe(false) + expect(experiments.isReReadAfterEditEnabled(config, "multiApplyDiff")).toBe(false) + expect(experiments.isReReadAfterEditEnabled(config, "writeToFile")).toBe(false) + expect(experiments.isReReadAfterEditEnabled(config, "insertContent")).toBe(false) + expect(experiments.isReReadAfterEditEnabled(config, "searchAndReplace")).toBe(false) + }) + }) +}) diff --git a/src/shared/__tests__/experiments-reReadAfterEdit.spec.ts b/src/shared/__tests__/experiments-reReadAfterEdit.spec.ts new file mode 100644 index 000000000000..646dc6d9d6fb --- /dev/null +++ b/src/shared/__tests__/experiments-reReadAfterEdit.spec.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from "vitest" +import { EXPERIMENT_IDS, experiments, experimentDefault } from "../experiments" + +describe("RE_READ_AFTER_EDIT experiment", () => { + it("should include RE_READ_AFTER_EDIT in EXPERIMENT_IDS", () => { + expect(EXPERIMENT_IDS.RE_READ_AFTER_EDIT).toBe("reReadAfterEdit") + }) + + it("should have RE_READ_AFTER_EDIT in default configuration", () => { + expect(experimentDefault.reReadAfterEdit).toBe(false) + }) + + it("should correctly check if RE_READ_AFTER_EDIT is enabled", () => { + const disabledConfig = { reReadAfterEdit: false } + expect(experiments.isEnabled(disabledConfig, EXPERIMENT_IDS.RE_READ_AFTER_EDIT)).toBe(false) + + const enabledConfig = { reReadAfterEdit: true } + expect(experiments.isEnabled(enabledConfig, EXPERIMENT_IDS.RE_READ_AFTER_EDIT)).toBe(true) + + const emptyConfig = {} + expect(experiments.isEnabled(emptyConfig, EXPERIMENT_IDS.RE_READ_AFTER_EDIT)).toBe(false) + }) +}) diff --git a/src/shared/__tests__/experiments.spec.ts b/src/shared/__tests__/experiments.spec.ts index 8a3c30044163..3c7a1ba6b517 100644 --- a/src/shared/__tests__/experiments.spec.ts +++ b/src/shared/__tests__/experiments.spec.ts @@ -1,6 +1,6 @@ // npx vitest run src/shared/__tests__/experiments.spec.ts -import type { ExperimentId } from "@roo-code/types" +import type { ExperimentId, Experiments as ExperimentsType } from "@roo-code/types" import { EXPERIMENT_IDS, experimentConfigsMap, experiments as Experiments } from "../experiments" @@ -25,36 +25,51 @@ describe("experiments", () => { describe("isEnabled", () => { it("returns false when POWER_STEERING experiment is not enabled", () => { - const experiments: Record = { + const experiments: ExperimentsType = { powerSteering: false, multiFileApplyDiff: false, preventFocusDisruption: false, imageGeneration: false, runSlashCommand: false, + reReadAfterEdit: false, + reReadAfterEditGranular: undefined, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) }) it("returns true when experiment POWER_STEERING is enabled", () => { - const experiments: Record = { + const experiments: ExperimentsType = { powerSteering: true, multiFileApplyDiff: false, preventFocusDisruption: false, imageGeneration: false, runSlashCommand: false, + reReadAfterEdit: false, + reReadAfterEditGranular: undefined, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true) }) it("returns false when experiment is not present", () => { - const experiments: Record = { + const experiments: ExperimentsType = { powerSteering: false, multiFileApplyDiff: false, preventFocusDisruption: false, imageGeneration: false, runSlashCommand: false, + reReadAfterEdit: false, + reReadAfterEditGranular: undefined, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false) }) }) + + describe("RE_READ_AFTER_EDIT_GRANULAR", () => { + it("is configured correctly", () => { + expect(EXPERIMENT_IDS.RE_READ_AFTER_EDIT_GRANULAR).toBe("reReadAfterEditGranular") + expect(experimentConfigsMap.RE_READ_AFTER_EDIT_GRANULAR).toMatchObject({ + enabled: false, + }) + }) + }) }) diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts index 90495c56b70b..7becf8b776bf 100644 --- a/src/shared/experiments.ts +++ b/src/shared/experiments.ts @@ -1,4 +1,12 @@ -import type { AssertEqual, Equals, Keys, Values, ExperimentId, Experiments } from "@roo-code/types" +import type { + AssertEqual, + Equals, + Keys, + Values, + ExperimentId, + Experiments, + ReReadAfterEditGranular, +} from "@roo-code/types" export const EXPERIMENT_IDS = { MULTI_FILE_APPLY_DIFF: "multiFileApplyDiff", @@ -6,6 +14,8 @@ export const EXPERIMENT_IDS = { PREVENT_FOCUS_DISRUPTION: "preventFocusDisruption", IMAGE_GENERATION: "imageGeneration", RUN_SLASH_COMMAND: "runSlashCommand", + RE_READ_AFTER_EDIT: "reReadAfterEdit", + RE_READ_AFTER_EDIT_GRANULAR: "reReadAfterEditGranular", } as const satisfies Record type _AssertExperimentIds = AssertEqual>> @@ -13,7 +23,7 @@ type _AssertExperimentIds = AssertEqual interface ExperimentConfig { - enabled: boolean + enabled: boolean | ReReadAfterEditGranular } export const experimentConfigsMap: Record = { @@ -22,6 +32,16 @@ export const experimentConfigsMap: Record = { PREVENT_FOCUS_DISRUPTION: { enabled: false }, IMAGE_GENERATION: { enabled: false }, RUN_SLASH_COMMAND: { enabled: false }, + RE_READ_AFTER_EDIT: { enabled: false }, + RE_READ_AFTER_EDIT_GRANULAR: { + enabled: { + applyDiff: false, + multiApplyDiff: false, + writeToFile: false, + insertContent: false, + searchAndReplace: false, + }, + }, } export const experimentDefault = Object.fromEntries( @@ -29,9 +49,23 @@ export const experimentDefault = Object.fromEntries( EXPERIMENT_IDS[_ as keyof typeof EXPERIMENT_IDS] as ExperimentId, config.enabled, ]), -) as Record +) as Experiments export const experiments = { get: (id: ExperimentKey): ExperimentConfig | undefined => experimentConfigsMap[id], isEnabled: (experimentsConfig: Experiments, id: ExperimentId) => experimentsConfig[id] ?? experimentDefault[id], + isReReadAfterEditEnabled: (experimentsConfig: Experiments, editType: keyof ReReadAfterEditGranular): boolean => { + // If the legacy RE_READ_AFTER_EDIT is enabled, it applies to all edit types + if (experimentsConfig.reReadAfterEdit) { + return true + } + + // Check if granular settings are enabled and if the specific edit type is enabled + const granularSettings = experimentsConfig.reReadAfterEditGranular + if (granularSettings && granularSettings[editType]) { + return true + } + + return false + }, } as const diff --git a/webview-ui/src/components/settings/ExperimentalSettings.tsx b/webview-ui/src/components/settings/ExperimentalSettings.tsx index 6883975d02e5..3f3cf97e572a 100644 --- a/webview-ui/src/components/settings/ExperimentalSettings.tsx +++ b/webview-ui/src/components/settings/ExperimentalSettings.tsx @@ -1,7 +1,7 @@ import { HTMLAttributes } from "react" import { FlaskConical } from "lucide-react" -import type { Experiments } from "@roo-code/types" +import type { Experiments, ReReadAfterEditGranular } from "@roo-code/types" import { EXPERIMENT_IDS, experimentConfigsMap } from "@roo/experiments" @@ -13,10 +13,12 @@ import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { ExperimentalFeature } from "./ExperimentalFeature" import { ImageGenerationSettings } from "./ImageGenerationSettings" +import { ReReadAfterEditGranularSettings } from "./ReReadAfterEditGranularSettings" type ExperimentalSettingsProps = HTMLAttributes & { experiments: Experiments setExperimentEnabled: SetExperimentEnabled + setReReadAfterEditGranular?: (settings: ReReadAfterEditGranular) => void apiConfiguration?: any setApiConfigurationField?: any openRouterImageApiKey?: string @@ -28,6 +30,7 @@ type ExperimentalSettingsProps = HTMLAttributes & { export const ExperimentalSettings = ({ experiments, setExperimentEnabled, + setReReadAfterEditGranular, apiConfiguration, setApiConfigurationField, openRouterImageApiKey, @@ -83,19 +86,38 @@ export const ExperimentalSettings = ({ /> ) } - return ( - - setExperimentEnabled( - EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS], - enabled, - ) - } - /> - ) + // Show granular settings for RE_READ_AFTER_EDIT + if (config[0] === "RE_READ_AFTER_EDIT_GRANULAR" && setReReadAfterEditGranular) { + return ( + + setExperimentEnabled(EXPERIMENT_IDS.RE_READ_AFTER_EDIT, enabled) + } + onGranularChange={setReReadAfterEditGranular} + /> + ) + } + // Skip the legacy RE_READ_AFTER_EDIT if granular is available + if (config[0] === "RE_READ_AFTER_EDIT" && setReReadAfterEditGranular) { + return null + } + const experimentId = EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS] + const value = experiments[experimentId] + // Only render if it's a boolean (not granular settings object) + if (typeof value === "boolean" || value === undefined) { + return ( + setExperimentEnabled(experimentId, enabled)} + /> + ) + } + return null })} diff --git a/webview-ui/src/components/settings/ReReadAfterEditGranularSettings.tsx b/webview-ui/src/components/settings/ReReadAfterEditGranularSettings.tsx new file mode 100644 index 000000000000..e9636083198c --- /dev/null +++ b/webview-ui/src/components/settings/ReReadAfterEditGranularSettings.tsx @@ -0,0 +1,88 @@ +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" +import { useAppTranslation } from "@/i18n/TranslationContext" +import type { ReReadAfterEditGranular } from "@roo-code/types" + +interface ReReadAfterEditGranularSettingsProps { + enabled: boolean + granularSettings?: ReReadAfterEditGranular + onChange: (enabled: boolean) => void + onGranularChange: (settings: ReReadAfterEditGranular) => void +} + +export const ReReadAfterEditGranularSettings = ({ + enabled, + granularSettings, + onChange, + onGranularChange, +}: ReReadAfterEditGranularSettingsProps) => { + const { t } = useAppTranslation() + + const editTypes = [ + { key: "applyDiff", label: "Apply Diff" }, + { key: "multiApplyDiff", label: "Multi-file Apply Diff" }, + { key: "writeToFile", label: "Write to File" }, + { key: "insertContent", label: "Insert Content" }, + { key: "searchAndReplace", label: "Search and Replace" }, + ] as const + + const handleGranularToggle = (editType: keyof ReReadAfterEditGranular, value: boolean) => { + const newSettings: ReReadAfterEditGranular = { + ...granularSettings, + [editType]: value, + } + onGranularChange(newSettings) + } + + const allChecked = editTypes.every((type) => granularSettings?.[type.key] === true) + const someChecked = editTypes.some((type) => granularSettings?.[type.key] === true) + const isIndeterminate = someChecked && !allChecked + + const handleMasterToggle = (checked: boolean) => { + const newSettings: ReReadAfterEditGranular = {} + editTypes.forEach((type) => { + newSettings[type.key] = checked + }) + onGranularChange(newSettings) + onChange(checked) + } + + return ( +
+
+
+ handleMasterToggle(e.target.checked)}> + + {t("settings:experimental.RE_READ_AFTER_EDIT_GRANULAR.name")} + + +
+

+ {t("settings:experimental.RE_READ_AFTER_EDIT_GRANULAR.description")} +

+
+ + {/* Show granular options when enabled */} + {(enabled || someChecked) && ( +
+

+ {t("settings:experimental.RE_READ_AFTER_EDIT_GRANULAR.selectEditTypes")} +

+ {editTypes.map((editType) => ( +
+ handleGranularToggle(editType.key, e.target.checked)}> + + {t(`settings:experimental.RE_READ_AFTER_EDIT_GRANULAR.${editType.key}`)} + + +
+ ))} +
+ )} +
+ ) +} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 506eabc4cb50..8d5f0bde0660 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -27,7 +27,7 @@ import { Glasses, } from "lucide-react" -import type { ProviderSettings, ExperimentId, TelemetrySetting } from "@roo-code/types" +import type { ProviderSettings, ExperimentId, TelemetrySetting, ReReadAfterEditGranular } from "@roo-code/types" import { vscode } from "@src/utils/vscode" import { cn } from "@src/lib/utils" @@ -272,6 +272,26 @@ const SettingsView = forwardRef(({ onDone, t }) }, []) + const setReReadAfterEditGranular = useCallback((settings: ReReadAfterEditGranular) => { + setCachedState((prevState) => { + // Only set change detected if value actually changed + const previousStr = JSON.stringify(prevState.experiments?.reReadAfterEditGranular) + const newStr = JSON.stringify(settings) + + if (previousStr !== newStr) { + setChangeDetected(true) + } + + return { + ...prevState, + experiments: { + ...prevState.experiments, + reReadAfterEditGranular: settings, + }, + } + }) + }, []) + const setTelemetrySetting = useCallback((setting: TelemetrySetting) => { setCachedState((prevState) => { if (prevState.telemetrySetting === setting) { @@ -799,6 +819,7 @@ const SettingsView = forwardRef(({ onDone, t {activeTab === "experimental" && ( { writeDelayMs: 1000, requestDelaySeconds: 5, mode: "default", - experiments: {} as Record, + experiments: {} as any, customModes: [], maxOpenTabsContext: 20, maxWorkspaceFiles: 100, @@ -220,7 +220,7 @@ describe("mergeExtensionState", () => { const prevState: ExtensionState = { ...baseState, apiConfiguration: { modelMaxTokens: 1234, modelMaxThinkingTokens: 123 }, - experiments: {} as Record, + experiments: {} as any, checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS - 5, } @@ -237,7 +237,15 @@ describe("mergeExtensionState", () => { newTaskRequireTodos: false, imageGeneration: false, runSlashCommand: false, - } as Record, + reReadAfterEdit: false, + reReadAfterEditGranular: { + applyDiff: false, + multiApplyDiff: false, + writeToFile: false, + insertContent: false, + searchAndReplace: false, + }, + } as any, checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS + 5, } @@ -258,6 +266,14 @@ describe("mergeExtensionState", () => { newTaskRequireTodos: false, imageGeneration: false, runSlashCommand: false, + reReadAfterEdit: false, + reReadAfterEditGranular: { + applyDiff: false, + multiApplyDiff: false, + writeToFile: false, + insertContent: false, + searchAndReplace: false, + }, }) }) }) diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 40a08e113fef..4f539fda5205 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -775,6 +775,20 @@ "RUN_SLASH_COMMAND": { "name": "Enable model-initiated slash commands", "description": "When enabled, Roo can run your slash commands to execute workflows." + }, + "RE_READ_AFTER_EDIT": { + "name": "Suggest review after file edits", + "description": "When enabled, Roo will suggest reviewing changes after editing files. This helps catch errors that might be introduced during editing by prompting to review the changes." + }, + "RE_READ_AFTER_EDIT_GRANULAR": { + "name": "Suggest review after file edits", + "description": "Select which edit operations should prompt Roo to review changes. This helps catch errors introduced during editing.", + "selectEditTypes": "Choose edit types to review:", + "applyDiff": "Apply Diff (search and replace blocks)", + "multiApplyDiff": "Multi-file Apply Diff", + "writeToFile": "Write to File (complete file rewrites)", + "insertContent": "Insert Content (add lines to files)", + "searchAndReplace": "Search and Replace (find and replace text)" } }, "promptCaching": {