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/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index fd0aca66cb7..1688ca57216 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -93,6 +93,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction + new Set([ + "editedExistingFile", + "appliedDiff", + "newFileCreated", + "insertContent", + "searchAndReplace", + "search_and_replace", + ]), + [], + ) + + useEffect(() => { + if (!autoExpandDiffs) return + + const newExpansions: Record = {} + + for (const msg of groupedMessages) { + // Skip messages already tracked in expandedRows + if (expandedRows[msg.ts] !== undefined) continue + + if (msg.type === "ask" && msg.ask === "tool") { + try { + const tool = JSON.parse(msg.text || "{}") + // Handle both single diff tools and batch diff messages + if (DIFF_TOOL_NAMES.has(tool.tool) || tool.tool === "batchDiffApproval") { + newExpansions[msg.ts] = true + } + } catch { + // ignore parse errors + } + } + } + + if (Object.keys(newExpansions).length > 0) { + setExpandedRows((prev) => ({ ...prev, ...newExpansions })) + } + }, [autoExpandDiffs, groupedMessages, expandedRows, DIFF_TOOL_NAMES]) + // Scroll lifecycle is managed by a dedicated hook to keep ChatView focused // on message handling and UI orchestration. const { diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 47e087615e3..5f4cd3c90c5 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -199,6 +199,7 @@ const SettingsView = forwardRef(({ 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..b9576b8d332 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,10 @@ export const UISettings = ({ }) } + const handleAutoExpandDiffsChange = (value: boolean) => { + setCachedStateField("autoExpandDiffs", value) + } + const handleEnterBehaviorChange = (requireCtrlEnter: boolean) => { const newBehavior = requireCtrlEnter ? "newline" : "send" setCachedStateField("enterBehavior", newBehavior) @@ -72,6 +78,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..a3b52a1f136 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(), } diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index ce7a607d9a8..d7ea28dd1c6 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -235,6 +235,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 diffs enterBehavior: "send", // Default: Enter sends, Shift+Enter creates newline cloudUserInfo: null, cloudIsAuthenticated: false, @@ -488,6 +489,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode const contextValue: ExtensionStateContextType = { ...state, reasoningBlockCollapsed: state.reasoningBlockCollapsed ?? true, + autoExpandDiffs: state.autoExpandDiffs ?? false, didHydrateState, showWelcome, theme, diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 183cd663e31..d711f1c5956 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 chat messages", + "description": "When enabled, file edit diffs will be automatically expanded instead of collapsed behind the filename" + }, "requireCtrlEnterToSend": { "label": "Require {{primaryMod}}+Enter to send messages", "description": "When enabled, you must press {{primaryMod}}+Enter to send messages instead of just Enter"