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
This commit is contained in:
Roo Code 2026-04-19 19:50:26 +00:00
parent cb83656718
commit 98cd31d007
11 changed files with 117 additions and 1 deletions

View file

@ -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)

View file

@ -299,6 +299,7 @@ export type ExtensionState = Pick<
| "openRouterImageGenerationSelectedModel"
| "includeTaskHistoryInEnhance"
| "reasoningBlockCollapsed"
| "autoExpandDiffs"
| "enterBehavior"
| "includeCurrentTime"
| "includeCurrentCost"

View file

@ -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,

View file

@ -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<ChatViewRef, ChatViewProps> = (
@ -93,6 +124,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
cloudIsAuthenticated,
messageQueue = [],
showWorktreesInHomeScreen,
autoExpandDiffs,
} = useExtensionState()
// Show a WarningRow when the user sends a message with a retired provider.
@ -1403,7 +1435,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
<ChatRow
key={messageOrGroup.ts}
message={messageOrGroup}
isExpanded={expandedRows[messageOrGroup.ts] || false}
isExpanded={
expandedRows[messageOrGroup.ts] !== undefined
? expandedRows[messageOrGroup.ts]
: autoExpandDiffs
? isDiffToolMessage(messageOrGroup)
: false
}
onToggleExpand={toggleRowExpansion} // This was already stabilized
lastModifiedMessage={modifiedMessages.at(-1)} // Original direct access
isLast={index === groupedMessages.length - 1} // Original direct access
@ -1447,6 +1485,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
isFollowUpAutoApprovalPaused,
enableButtons,
primaryButtonText,
autoExpandDiffs,
],
)

View file

@ -199,6 +199,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
openRouterImageApiKey,
openRouterImageGenerationSelectedModel,
reasoningBlockCollapsed,
autoExpandDiffs,
enterBehavior,
includeCurrentTime,
includeCurrentCost,
@ -412,6 +413,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ 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<SettingsViewRef, SettingsViewProps>(({ onDone, t
{renderTab === "ui" && (
<UISettings
reasoningBlockCollapsed={reasoningBlockCollapsed ?? true}
autoExpandDiffs={autoExpandDiffs ?? false}
enterBehavior={enterBehavior ?? "send"}
setCachedStateField={setCachedStateField}
/>

View file

@ -11,12 +11,14 @@ import { ExtensionStateContextType } from "@/context/ExtensionStateContext"
interface UISettingsProps extends HTMLAttributes<HTMLDivElement> {
reasoningBlockCollapsed: boolean
autoExpandDiffs: boolean
enterBehavior: "send" | "newline"
setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
}
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 = ({
</div>
</SearchableSetting>
{/* Auto-Expand Diffs Setting */}
<SearchableSetting
settingId="ui-auto-expand-diffs"
section="ui"
label={t("settings:ui.autoExpandDiffs.label")}>
<div className="flex flex-col gap-1">
<VSCodeCheckbox
checked={autoExpandDiffs}
onChange={(e: any) => handleAutoExpandDiffsChange(e.target.checked)}
data-testid="auto-expand-diffs-checkbox">
<span className="font-medium">{t("settings:ui.autoExpandDiffs.label")}</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm ml-5 mt-1">
{t("settings:ui.autoExpandDiffs.description")}
</div>
</div>
</SearchableSetting>
{/* Enter Key Behavior Setting */}
<SearchableSetting
settingId="ui-enter-behavior"

View file

@ -302,6 +302,7 @@ describe("SettingsView - Change Detection Fix", () => {
openRouterImageApiKey: undefined,
openRouterImageGenerationSelectedModel: undefined,
reasoningBlockCollapsed: true,
autoExpandDiffs: false,
...overrides,
})

View file

@ -307,6 +307,7 @@ describe("SettingsView - Unsaved Changes Detection", () => {
openRouterImageApiKey: undefined,
openRouterImageGenerationSelectedModel: undefined,
reasoningBlockCollapsed: true,
autoExpandDiffs: false,
}
beforeEach(() => {

View file

@ -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(<UISettings {...defaultProps} reasoningBlockCollapsed={true} />)
expect(checkbox.checked).toBe(true)
})
it("renders the auto-expand diffs checkbox", () => {
const { getByTestId } = render(<UISettings {...defaultProps} />)
const checkbox = getByTestId("auto-expand-diffs-checkbox")
expect(checkbox).toBeTruthy()
})
it("displays the correct initial state for auto-expand diffs", () => {
const { getByTestId } = render(<UISettings {...defaultProps} autoExpandDiffs={true} />)
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(<UISettings {...defaultProps} setCachedStateField={setCachedStateField} />)
const checkbox = getByTestId("auto-expand-diffs-checkbox")
fireEvent.click(checkbox)
await waitFor(() => {
expect(setCachedStateField).toHaveBeenCalledWith("autoExpandDiffs", true)
})
})
})

View file

@ -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 })),

View file

@ -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"