From 98cd31d007bd4c297737d958703ed6860cc9d484 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sun, 19 Apr 2026 19:50:26 +0000 Subject: [PATCH] feat: add setting to auto-expand diffs in edit messages Adds a new "Auto-expand diffs in edit messages" setting under Settings > UI that, when enabled, automatically expands code diffs in "Roo wants to edit this file" chat messages instead of requiring users to click each one. The setting: - Defaults to false (preserving current collapsed behavior) - Respects user toggles (clicking to collapse overrides auto-expand) - Works with all diff tool types (editedExistingFile, appliedDiff, newFileCreated, searchAndReplace, insertContent, etc.) - Diffs still respect the existing 300px max-height with scrollbar Closes #10955 --- packages/types/src/global-settings.ts | 5 +++ packages/types/src/vscode-extension-host.ts | 1 + src/core/webview/ClineProvider.ts | 3 ++ webview-ui/src/components/chat/ChatView.tsx | 41 ++++++++++++++++++- .../src/components/settings/SettingsView.tsx | 3 ++ .../src/components/settings/UISettings.tsx | 29 +++++++++++++ .../SettingsView.change-detection.spec.tsx | 1 + .../SettingsView.unsaved-changes.spec.tsx | 1 + .../settings/__tests__/UISettings.spec.tsx | 25 +++++++++++ .../src/context/ExtensionStateContext.tsx | 5 +++ webview-ui/src/i18n/locales/en/settings.json | 4 ++ 11 files changed, 117 insertions(+), 1 deletion(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 288f6c2118c..10fe10a4c1a 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -201,6 +201,11 @@ export const globalSettingsSchema = z.object({ includeTaskHistoryInEnhance: z.boolean().optional(), historyPreviewCollapsed: z.boolean().optional(), reasoningBlockCollapsed: z.boolean().optional(), + /** + * Whether to auto-expand diffs in "Roo wants to edit this file" chat messages. + * @default false + */ + autoExpandDiffs: z.boolean().optional(), /** * Controls the keyboard behavior for sending messages in the chat input. * - "send": Enter sends message, Shift+Enter creates newline (default) diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index b20539afe49..702cf3fb7d8 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -299,6 +299,7 @@ export type ExtensionState = Pick< | "openRouterImageGenerationSelectedModel" | "includeTaskHistoryInEnhance" | "reasoningBlockCollapsed" + | "autoExpandDiffs" | "enterBehavior" | "includeCurrentTime" | "includeCurrentCost" diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 2ffe421c095..b9db8763aeb 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2184,6 +2184,7 @@ export class ClineProvider maxTotalImageSize, historyPreviewCollapsed, reasoningBlockCollapsed, + autoExpandDiffs, enterBehavior, cloudUserInfo, cloudIsAuthenticated, @@ -2310,6 +2311,7 @@ export class ClineProvider settingsImportedAt: this.settingsImportedAt, historyPreviewCollapsed: historyPreviewCollapsed ?? false, reasoningBlockCollapsed: reasoningBlockCollapsed ?? true, + autoExpandDiffs: autoExpandDiffs ?? false, enterBehavior: enterBehavior ?? "send", cloudUserInfo, cloudIsAuthenticated: cloudIsAuthenticated ?? false, @@ -2533,6 +2535,7 @@ export class ClineProvider maxTotalImageSize: stateValues.maxTotalImageSize ?? 20, historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false, reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true, + autoExpandDiffs: stateValues.autoExpandDiffs ?? false, enterBehavior: stateValues.enterBehavior ?? "send", cloudUserInfo, cloudIsAuthenticated, diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index fd0aca66cb7..bd54b39b6fc 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -63,6 +63,37 @@ export interface ChatViewRef { export const MAX_IMAGES_PER_MESSAGE = 20 // This is the Anthropic limit. +/** Tool names that produce file diffs in the chat UI. */ +const DIFF_TOOL_NAMES = new Set([ + "editedExistingFile", + "appliedDiff", + "newFileCreated", + "searchAndReplace", + "search_and_replace", + "search_replace", + "edit", + "edit_file", + "apply_patch", + "apply_diff", + "insertContent", +]) + +/** + * Returns true when a message represents a diff-tool invocation that should + * be auto-expanded when the `autoExpandDiffs` setting is enabled. + */ +function isDiffToolMessage(message: ClineMessage): boolean { + if (message.ask !== "tool") { + return false + } + try { + const tool = JSON.parse(message.text || "{}") as ClineSayTool + return DIFF_TOOL_NAMES.has(tool.tool as string) + } catch { + return false + } +} + const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0 const ChatViewComponent: React.ForwardRefRenderFunction = ( @@ -93,6 +124,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction(({ onDone, t openRouterImageApiKey, openRouterImageGenerationSelectedModel, reasoningBlockCollapsed, + autoExpandDiffs, enterBehavior, includeCurrentTime, includeCurrentCost, @@ -412,6 +413,7 @@ const SettingsView = forwardRef(({ onDone, t followupAutoApproveTimeoutMs, includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true, reasoningBlockCollapsed: reasoningBlockCollapsed ?? true, + autoExpandDiffs: autoExpandDiffs ?? false, enterBehavior: enterBehavior ?? "send", includeCurrentTime: includeCurrentTime ?? true, includeCurrentCost: includeCurrentCost ?? true, @@ -891,6 +893,7 @@ const SettingsView = forwardRef(({ onDone, t {renderTab === "ui" && ( diff --git a/webview-ui/src/components/settings/UISettings.tsx b/webview-ui/src/components/settings/UISettings.tsx index a3488dc59e1..f381770e253 100644 --- a/webview-ui/src/components/settings/UISettings.tsx +++ b/webview-ui/src/components/settings/UISettings.tsx @@ -11,12 +11,14 @@ import { ExtensionStateContextType } from "@/context/ExtensionStateContext" interface UISettingsProps extends HTMLAttributes { reasoningBlockCollapsed: boolean + autoExpandDiffs: boolean enterBehavior: "send" | "newline" setCachedStateField: SetCachedStateField } export const UISettings = ({ reasoningBlockCollapsed, + autoExpandDiffs, enterBehavior, setCachedStateField, ...props @@ -38,6 +40,15 @@ export const UISettings = ({ }) } + const handleAutoExpandDiffsChange = (value: boolean) => { + setCachedStateField("autoExpandDiffs", value) + + // Track telemetry event + telemetryClient.capture("ui_settings_auto_expand_diffs_changed", { + enabled: value, + }) + } + const handleEnterBehaviorChange = (requireCtrlEnter: boolean) => { const newBehavior = requireCtrlEnter ? "newline" : "send" setCachedStateField("enterBehavior", newBehavior) @@ -72,6 +83,24 @@ export const UISettings = ({ + {/* Auto-Expand Diffs Setting */} + +
+ handleAutoExpandDiffsChange(e.target.checked)} + data-testid="auto-expand-diffs-checkbox"> + {t("settings:ui.autoExpandDiffs.label")} + +
+ {t("settings:ui.autoExpandDiffs.description")} +
+
+
+ {/* Enter Key Behavior Setting */} { openRouterImageApiKey: undefined, openRouterImageGenerationSelectedModel: undefined, reasoningBlockCollapsed: true, + autoExpandDiffs: false, ...overrides, }) diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx index 83be2509d08..c04024116fd 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.unsaved-changes.spec.tsx @@ -307,6 +307,7 @@ describe("SettingsView - Unsaved Changes Detection", () => { openRouterImageApiKey: undefined, openRouterImageGenerationSelectedModel: undefined, reasoningBlockCollapsed: true, + autoExpandDiffs: false, } beforeEach(() => { diff --git a/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx b/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx index 2a21a410b38..e0d8cd68456 100644 --- a/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx @@ -5,6 +5,7 @@ import { UISettings } from "../UISettings" describe("UISettings", () => { const defaultProps = { reasoningBlockCollapsed: false, + autoExpandDiffs: false, enterBehavior: "send" as const, setCachedStateField: vi.fn(), } @@ -41,4 +42,28 @@ describe("UISettings", () => { rerender() expect(checkbox.checked).toBe(true) }) + + it("renders the auto-expand diffs checkbox", () => { + const { getByTestId } = render() + const checkbox = getByTestId("auto-expand-diffs-checkbox") + expect(checkbox).toBeTruthy() + }) + + it("displays the correct initial state for auto-expand diffs", () => { + const { getByTestId } = render() + const checkbox = getByTestId("auto-expand-diffs-checkbox") as HTMLInputElement + expect(checkbox.checked).toBe(true) + }) + + it("calls setCachedStateField when auto-expand diffs checkbox is toggled", async () => { + const setCachedStateField = vi.fn() + const { getByTestId } = render() + + const checkbox = getByTestId("auto-expand-diffs-checkbox") + fireEvent.click(checkbox) + + await waitFor(() => { + expect(setCachedStateField).toHaveBeenCalledWith("autoExpandDiffs", true) + }) + }) }) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index ce7a607d9a8..91eadcf2820 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -124,6 +124,8 @@ export interface ExtensionStateContextType extends ExtensionState { togglePinnedApiConfig: (configName: string) => void setHistoryPreviewCollapsed: (value: boolean) => void setReasoningBlockCollapsed: (value: boolean) => void + autoExpandDiffs?: boolean + setAutoExpandDiffs: (value: boolean) => void enterBehavior?: "send" | "newline" setEnterBehavior: (value: "send" | "newline") => void autoCondenseContext: boolean @@ -235,6 +237,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode terminalZdotdir: false, // Default ZDOTDIR handling setting historyPreviewCollapsed: false, // Initialize the new state (default to expanded) reasoningBlockCollapsed: true, // Default to collapsed + autoExpandDiffs: false, // Default to collapsed (current behavior) enterBehavior: "send", // Default: Enter sends, Shift+Enter creates newline cloudUserInfo: null, cloudIsAuthenticated: false, @@ -584,6 +587,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setState((prevState) => ({ ...prevState, historyPreviewCollapsed: value })), setReasoningBlockCollapsed: (value) => setState((prevState) => ({ ...prevState, reasoningBlockCollapsed: value })), + autoExpandDiffs: state.autoExpandDiffs ?? false, + setAutoExpandDiffs: (value) => setState((prevState) => ({ ...prevState, autoExpandDiffs: value })), enterBehavior: state.enterBehavior ?? "send", setEnterBehavior: (value) => setState((prevState) => ({ ...prevState, enterBehavior: value })), setHasOpenedModeSelector: (value) => setState((prevState) => ({ ...prevState, hasOpenedModeSelector: value })), diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 183cd663e31..a57d7773b6d 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -160,6 +160,10 @@ "label": "Collapse Thinking messages by default", "description": "When enabled, thinking blocks will be collapsed by default until you interact with them" }, + "autoExpandDiffs": { + "label": "Auto-expand diffs in edit messages", + "description": "When enabled, code diffs in \"Roo wants to edit this file\" messages will be expanded by default instead of collapsed" + }, "requireCtrlEnterToSend": { "label": "Require {{primaryMod}}+Enter to send messages", "description": "When enabled, you must press {{primaryMod}}+Enter to send messages instead of just Enter"