diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 1f43d3093..706c75cd1 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -22,6 +22,20 @@ import { languagesSchema } from "./vscode.js" */ export const DEFAULT_WRITE_DELAY_MS = 1000 +/** + * Default values for the "auto-close files Zoo opened" settings. + * + * These are defined once here and consumed by every site that reads the setting + * (DiffViewProvider save/revert, ClineProvider state serialization, and the + * UISettings checkboxes) so there is a single source of truth for the default + * behavior. Auto-closing edited tabs is opt-in: by default, files Zoo edits stay + * open in the editor (the long-standing behavior). Users who want to save + * context tokens by closing the edited tab after each edit can enable it. + */ +export const DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES = false +export const DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED = false +export const DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES = false + /** * Default fuzzy matching threshold for the multi-search-replace diff strategy. * A value of 1.0 (exact match) is used by default for safety, especially when diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index ee7364963..6f11cf4c7 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -42,6 +42,9 @@ import { openRouterDefaultModelId, DEFAULT_WRITE_DELAY_MS, DEFAULT_DIFF_FUZZY_THRESHOLD, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, ORGANIZATION_ALLOW_ALL, DEFAULT_MODES, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, @@ -2471,9 +2474,10 @@ export class ClineProvider imageGenerationProvider, openRouterImageApiKey, openRouterImageGenerationSelectedModel, - autoCloseZooOpenedFiles: autoCloseZooOpenedFiles ?? true, - autoCloseZooOpenedFilesAfterUserEdited: autoCloseZooOpenedFilesAfterUserEdited ?? false, - autoCloseZooOpenedNewFiles: autoCloseZooOpenedNewFiles ?? false, + autoCloseZooOpenedFiles: autoCloseZooOpenedFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + autoCloseZooOpenedFilesAfterUserEdited: + autoCloseZooOpenedFilesAfterUserEdited ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + autoCloseZooOpenedNewFiles: autoCloseZooOpenedNewFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, openAiCodexIsAuthenticated: await (async () => { try { const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth") diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index cb4218482..1904b46bd 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -1072,7 +1072,7 @@ describe("ClineProvider", () => { expect(state.autoCloseZooOpenedNewFiles).toBe(true) }) - it("getStateToPostToWebview defaults autoCloseZooOpenedFiles to true when unset", async () => { + it("getStateToPostToWebview defaults autoCloseZooOpenedFiles to false when unset", async () => { await provider.resolveWebviewView(mockWebviewView) // Ensure the settings are not set. @@ -1082,8 +1082,8 @@ describe("ClineProvider", () => { const state = await provider.getStateToPostToWebview() - // Unset values should default to their documented defaults. - expect(state.autoCloseZooOpenedFiles).toBe(true) + // Unset values should default to their documented defaults (opt-in). + expect(state.autoCloseZooOpenedFiles).toBe(false) expect(state.autoCloseZooOpenedFilesAfterUserEdited).toBe(false) expect(state.autoCloseZooOpenedNewFiles).toBe(false) }) diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index ba2ae4fc0..bb3368f06 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -5,7 +5,13 @@ import * as diff from "diff" import stripBom from "strip-bom" import delay from "delay" -import { type ClineSayTool, DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" +import { + type ClineSayTool, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, + DEFAULT_WRITE_DELAY_MS, +} from "@roo-code/types" import { createDirectoriesForFile } from "../../utils/fs" import { arePathsEqual, getReadablePath } from "../../utils/path" @@ -352,9 +358,9 @@ export class DiffViewProvider { await this.keepOrCloseEditedFile( absolutePath, this.userTouchedDiffEditor, - saveState?.autoCloseZooOpenedFiles ?? true, - saveState?.autoCloseZooOpenedFilesAfterUserEdited ?? false, - saveState?.autoCloseZooOpenedNewFiles ?? false, + saveState?.autoCloseZooOpenedFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + saveState?.autoCloseZooOpenedFilesAfterUserEdited ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + saveState?.autoCloseZooOpenedNewFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, ) // Restore any preview tabs the diff evicted, reconstructing the user's @@ -563,9 +569,10 @@ export class DiffViewProvider { await this.keepOrCloseEditedFile( absolutePath, false, - revertState?.autoCloseZooOpenedFiles ?? true, - revertState?.autoCloseZooOpenedFilesAfterUserEdited ?? false, - revertState?.autoCloseZooOpenedNewFiles ?? false, + revertState?.autoCloseZooOpenedFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + revertState?.autoCloseZooOpenedFilesAfterUserEdited ?? + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + revertState?.autoCloseZooOpenedNewFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, ) } @@ -647,16 +654,20 @@ export class DiffViewProvider { // refinement of the base auto-close, so it has no effect when the base // setting is off. // 4. autoCloseZooOpenedFiles=false -> keep the transiently-opened tab. - // 5. Default -> close the transiently-opened tab (current behavior preserved). + // 5. autoCloseZooOpenedFiles=true -> close the transiently-opened tab. + // + // The default value of autoCloseZooOpenedFiles is false (opt-in), so by default + // branch 4 applies and the edited file stays open. See DEFAULT_AUTO_CLOSE_* in + // @roo-code/types for the single source of truth for these defaults. // // keepIfTouchedDiff is passed as true from saveChanges() when the user clicked // or typed inside the diff editor itself. private async keepOrCloseEditedFile( absolutePath: string, keepIfTouchedDiff = false, - autoCloseZooOpenedFiles = true, - autoCloseZooOpenedFilesAfterUserEdited = false, - autoCloseZooOpenedNewFiles = false, + autoCloseZooOpenedFiles = DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + autoCloseZooOpenedFilesAfterUserEdited = DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + autoCloseZooOpenedNewFiles = DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, ): Promise { // Files the user already had open are never auto-closed. if (this.documentWasOpen) { @@ -682,7 +693,8 @@ export class DiffViewProvider { return } - // Transient tab opened by Zoo: close by default, keep only when opted out. + // Transient tab opened by Zoo: close only when auto-close is enabled (opt-in); + // keep and re-show it otherwise (the default). if (autoCloseZooOpenedFiles) { await this.closeFileTab(absolutePath) } else { diff --git a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts index f7bf0d708..d56338b6b 100644 --- a/src/integrations/editor/__tests__/DiffViewProvider.spec.ts +++ b/src/integrations/editor/__tests__/DiffViewProvider.spec.ts @@ -154,6 +154,11 @@ describe("DiffViewProvider", () => { getState: vi.fn().mockResolvedValue({ includeDiagnosticMessages: true, maxDiagnosticMessages: 50, + // Auto-closing edited tabs is opt-in by default; the legacy + // "close/keep behavior" suite below asserts the close path, so + // enable it here. The opt-in default itself is covered by the + // dedicated "auto-close settings decision table" suite. + autoCloseZooOpenedFiles: true, }), }), }, @@ -1711,7 +1716,24 @@ describe("DiffViewProvider", () => { expect(vscode.window.showTextDocument).toHaveBeenCalled() }) - it("transient tab is closed when autoCloseZooOpenedFiles is true (default)", async () => { + it("transient tab is kept by default when autoCloseZooOpenedFiles is unset (opt-in)", async () => { + // Empty state -> autoCloseZooOpenedFiles is undefined and falls back to the + // centralized default (false), so an untouched transient tab is kept. + const provider = setupProvider({}) + const closeFileTab = vi.fn().mockResolvedValue(undefined) + ;(provider as any).closeFileTab = closeFileTab + ;(provider as any).documentWasOpen = false + ;(provider as any).userTouchedDocument = false + ;(provider as any).userTouchedDiffEditor = false + vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) + + await provider.saveChanges(false) + + expect(closeFileTab).not.toHaveBeenCalled() + expect(vscode.window.showTextDocument).toHaveBeenCalled() + }) + + it("transient tab is closed when autoCloseZooOpenedFiles is true", async () => { const provider = setupProvider({ autoCloseZooOpenedFiles: true }) const closeFileTab = vi.fn().mockResolvedValue(undefined) ;(provider as any).closeFileTab = closeFileTab @@ -1739,7 +1761,12 @@ describe("DiffViewProvider", () => { }) it("touched tab is closed when autoCloseZooOpenedFilesAfterUserEdited is true", async () => { - const provider = setupProvider({ autoCloseZooOpenedFilesAfterUserEdited: true }) + // The after-edit override only closes when the base auto-close is also + // enabled, so set both (the base default is now opt-in/false). + const provider = setupProvider({ + autoCloseZooOpenedFiles: true, + autoCloseZooOpenedFilesAfterUserEdited: true, + }) const closeFileTab = vi.fn().mockResolvedValue(undefined) ;(provider as any).closeFileTab = closeFileTab ;(provider as any).documentWasOpen = false @@ -1807,18 +1834,21 @@ describe("DiffViewProvider", () => { expect(vscode.window.showTextDocument).toHaveBeenCalled() }) - it("defaults preserve existing behavior when all settings are unset", async () => { - // No auto-close settings in state: transient tab should be closed (existing default). + it("defaults keep the transient tab open when all settings are unset", async () => { + // No auto-close settings in state: auto-closing is opt-in, so an + // untouched transient tab is kept and re-shown (long-standing behavior). const provider = setupProvider({}) const closeFileTab = vi.fn().mockResolvedValue(undefined) ;(provider as any).closeFileTab = closeFileTab ;(provider as any).documentWasOpen = false ;(provider as any).userTouchedDocument = false ;(provider as any).userTouchedDiffEditor = false + vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any) await provider.saveChanges(false) - expect(closeFileTab).toHaveBeenCalledWith(mockTargetPath) + expect(closeFileTab).not.toHaveBeenCalled() + expect(vscode.window.showTextDocument).toHaveBeenCalled() }) }) }) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index dda32aa6c..e712b837d 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -35,6 +35,9 @@ import { type ProviderSettings, type ExperimentId, type TelemetrySetting, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, ImageGenerationProvider, } from "@roo-code/types" @@ -425,9 +428,10 @@ const SettingsView = forwardRef(({ onDone, t includeCurrentTime: includeCurrentTime ?? true, includeCurrentCost: includeCurrentCost ?? true, maxGitStatusFiles: maxGitStatusFiles ?? 0, - autoCloseZooOpenedFiles: autoCloseZooOpenedFiles ?? true, - autoCloseZooOpenedFilesAfterUserEdited: autoCloseZooOpenedFilesAfterUserEdited ?? false, - autoCloseZooOpenedNewFiles: autoCloseZooOpenedNewFiles ?? false, + autoCloseZooOpenedFiles: autoCloseZooOpenedFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + autoCloseZooOpenedFilesAfterUserEdited: + autoCloseZooOpenedFilesAfterUserEdited ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + autoCloseZooOpenedNewFiles: autoCloseZooOpenedNewFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, profileThresholds, imageGenerationProvider, openRouterImageApiKey, diff --git a/webview-ui/src/components/settings/UISettings.tsx b/webview-ui/src/components/settings/UISettings.tsx index 2b3ab08a8..b40ca8b68 100644 --- a/webview-ui/src/components/settings/UISettings.tsx +++ b/webview-ui/src/components/settings/UISettings.tsx @@ -2,6 +2,11 @@ import { HTMLAttributes, useMemo } from "react" import { useAppTranslation } from "@/i18n/TranslationContext" import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { telemetryClient } from "@/utils/TelemetryClient" +import { + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED, + DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES, +} from "@roo-code/types" import { SetCachedStateField } from "./types" import { SectionHeader } from "./SectionHeader" @@ -160,7 +165,7 @@ export const UISettings = ({ label={t("settings:ui.autoCloseZooOpenedFiles.label")}>
setCachedStateField("autoCloseZooOpenedFiles", e.target.checked)} data-testid="auto-close-zoo-opened-files-checkbox"> {t("settings:ui.autoCloseZooOpenedFiles.label")} @@ -178,7 +183,10 @@ export const UISettings = ({ label={t("settings:ui.autoCloseZooOpenedFilesAfterUserEdited.label")}>
setCachedStateField("autoCloseZooOpenedFilesAfterUserEdited", e.target.checked) } @@ -200,7 +208,7 @@ export const UISettings = ({ label={t("settings:ui.autoCloseZooOpenedNewFiles.label")}>
setCachedStateField("autoCloseZooOpenedNewFiles", e.target.checked) } diff --git a/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx b/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx index 24593a223..4e61f0fcf 100644 --- a/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx @@ -120,6 +120,14 @@ describe("UISettings", () => { expect(checkbox.checked).toBe(false) }) + it("autoCloseZooOpenedFiles checkbox defaults to unchecked when prop is unset", () => { + // Omitting the prop simulates the opt-in default (false). A regression that + // flips the fallback back to `?? true` would make this checkbox checked. + const { getByTestId } = render() + const checkbox = getByTestId("auto-close-zoo-opened-files-checkbox") as HTMLInputElement + expect(checkbox.checked).toBe(false) + }) + it("calls setCachedStateField with autoCloseZooOpenedFiles when toggled", async () => { const setCachedStateField = vi.fn() const { getByTestId } = render(