Skip to content

Commit 1b26b6a

Browse files
authored
fix(diff-view): make auto-closing edited files opt-in (#720)
1 parent 6670962 commit 1b26b6a

8 files changed

Lines changed: 109 additions & 29 deletions

File tree

packages/types/src/global-settings.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ import { languagesSchema } from "./vscode.js"
2222
*/
2323
export const DEFAULT_WRITE_DELAY_MS = 1000
2424

25+
/**
26+
* Default values for the "auto-close files Zoo opened" settings.
27+
*
28+
* These are defined once here and consumed by every site that reads the setting
29+
* (DiffViewProvider save/revert, ClineProvider state serialization, and the
30+
* UISettings checkboxes) so there is a single source of truth for the default
31+
* behavior. Auto-closing edited tabs is opt-in: by default, files Zoo edits stay
32+
* open in the editor (the long-standing behavior). Users who want to save
33+
* context tokens by closing the edited tab after each edit can enable it.
34+
*/
35+
export const DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES = false
36+
export const DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED = false
37+
export const DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES = false
38+
2539
/**
2640
* Default fuzzy matching threshold for the multi-search-replace diff strategy.
2741
* A value of 1.0 (exact match) is used by default for safety, especially when

src/core/webview/ClineProvider.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ import {
4242
openRouterDefaultModelId,
4343
DEFAULT_WRITE_DELAY_MS,
4444
DEFAULT_DIFF_FUZZY_THRESHOLD,
45+
DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES,
46+
DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED,
47+
DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES,
4548
ORGANIZATION_ALLOW_ALL,
4649
DEFAULT_MODES,
4750
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
@@ -2471,9 +2474,10 @@ export class ClineProvider
24712474
imageGenerationProvider,
24722475
openRouterImageApiKey,
24732476
openRouterImageGenerationSelectedModel,
2474-
autoCloseZooOpenedFiles: autoCloseZooOpenedFiles ?? true,
2475-
autoCloseZooOpenedFilesAfterUserEdited: autoCloseZooOpenedFilesAfterUserEdited ?? false,
2476-
autoCloseZooOpenedNewFiles: autoCloseZooOpenedNewFiles ?? false,
2477+
autoCloseZooOpenedFiles: autoCloseZooOpenedFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES,
2478+
autoCloseZooOpenedFilesAfterUserEdited:
2479+
autoCloseZooOpenedFilesAfterUserEdited ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED,
2480+
autoCloseZooOpenedNewFiles: autoCloseZooOpenedNewFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES,
24772481
openAiCodexIsAuthenticated: await (async () => {
24782482
try {
24792483
const { openAiCodexOAuthManager } = await import("../../integrations/openai-codex/oauth")

src/core/webview/__tests__/ClineProvider.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1072,7 +1072,7 @@ describe("ClineProvider", () => {
10721072
expect(state.autoCloseZooOpenedNewFiles).toBe(true)
10731073
})
10741074

1075-
it("getStateToPostToWebview defaults autoCloseZooOpenedFiles to true when unset", async () => {
1075+
it("getStateToPostToWebview defaults autoCloseZooOpenedFiles to false when unset", async () => {
10761076
await provider.resolveWebviewView(mockWebviewView)
10771077

10781078
// Ensure the settings are not set.
@@ -1082,8 +1082,8 @@ describe("ClineProvider", () => {
10821082

10831083
const state = await provider.getStateToPostToWebview()
10841084

1085-
// Unset values should default to their documented defaults.
1086-
expect(state.autoCloseZooOpenedFiles).toBe(true)
1085+
// Unset values should default to their documented defaults (opt-in).
1086+
expect(state.autoCloseZooOpenedFiles).toBe(false)
10871087
expect(state.autoCloseZooOpenedFilesAfterUserEdited).toBe(false)
10881088
expect(state.autoCloseZooOpenedNewFiles).toBe(false)
10891089
})

src/integrations/editor/DiffViewProvider.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@ import * as diff from "diff"
55
import stripBom from "strip-bom"
66
import delay from "delay"
77

8-
import { type ClineSayTool, DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
8+
import {
9+
type ClineSayTool,
10+
DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES,
11+
DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED,
12+
DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES,
13+
DEFAULT_WRITE_DELAY_MS,
14+
} from "@roo-code/types"
915

1016
import { createDirectoriesForFile } from "../../utils/fs"
1117
import { arePathsEqual, getReadablePath } from "../../utils/path"
@@ -352,9 +358,9 @@ export class DiffViewProvider {
352358
await this.keepOrCloseEditedFile(
353359
absolutePath,
354360
this.userTouchedDiffEditor,
355-
saveState?.autoCloseZooOpenedFiles ?? true,
356-
saveState?.autoCloseZooOpenedFilesAfterUserEdited ?? false,
357-
saveState?.autoCloseZooOpenedNewFiles ?? false,
361+
saveState?.autoCloseZooOpenedFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES,
362+
saveState?.autoCloseZooOpenedFilesAfterUserEdited ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED,
363+
saveState?.autoCloseZooOpenedNewFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES,
358364
)
359365

360366
// Restore any preview tabs the diff evicted, reconstructing the user's
@@ -563,9 +569,10 @@ export class DiffViewProvider {
563569
await this.keepOrCloseEditedFile(
564570
absolutePath,
565571
false,
566-
revertState?.autoCloseZooOpenedFiles ?? true,
567-
revertState?.autoCloseZooOpenedFilesAfterUserEdited ?? false,
568-
revertState?.autoCloseZooOpenedNewFiles ?? false,
572+
revertState?.autoCloseZooOpenedFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES,
573+
revertState?.autoCloseZooOpenedFilesAfterUserEdited ??
574+
DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED,
575+
revertState?.autoCloseZooOpenedNewFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES,
569576
)
570577
}
571578

@@ -647,16 +654,20 @@ export class DiffViewProvider {
647654
// refinement of the base auto-close, so it has no effect when the base
648655
// setting is off.
649656
// 4. autoCloseZooOpenedFiles=false -> keep the transiently-opened tab.
650-
// 5. Default -> close the transiently-opened tab (current behavior preserved).
657+
// 5. autoCloseZooOpenedFiles=true -> close the transiently-opened tab.
658+
//
659+
// The default value of autoCloseZooOpenedFiles is false (opt-in), so by default
660+
// branch 4 applies and the edited file stays open. See DEFAULT_AUTO_CLOSE_* in
661+
// @roo-code/types for the single source of truth for these defaults.
651662
//
652663
// keepIfTouchedDiff is passed as true from saveChanges() when the user clicked
653664
// or typed inside the diff editor itself.
654665
private async keepOrCloseEditedFile(
655666
absolutePath: string,
656667
keepIfTouchedDiff = false,
657-
autoCloseZooOpenedFiles = true,
658-
autoCloseZooOpenedFilesAfterUserEdited = false,
659-
autoCloseZooOpenedNewFiles = false,
668+
autoCloseZooOpenedFiles = DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES,
669+
autoCloseZooOpenedFilesAfterUserEdited = DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED,
670+
autoCloseZooOpenedNewFiles = DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES,
660671
): Promise<void> {
661672
// Files the user already had open are never auto-closed.
662673
if (this.documentWasOpen) {
@@ -682,7 +693,8 @@ export class DiffViewProvider {
682693
return
683694
}
684695

685-
// Transient tab opened by Zoo: close by default, keep only when opted out.
696+
// Transient tab opened by Zoo: close only when auto-close is enabled (opt-in);
697+
// keep and re-show it otherwise (the default).
686698
if (autoCloseZooOpenedFiles) {
687699
await this.closeFileTab(absolutePath)
688700
} else {

src/integrations/editor/__tests__/DiffViewProvider.spec.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,11 @@ describe("DiffViewProvider", () => {
154154
getState: vi.fn().mockResolvedValue({
155155
includeDiagnosticMessages: true,
156156
maxDiagnosticMessages: 50,
157+
// Auto-closing edited tabs is opt-in by default; the legacy
158+
// "close/keep behavior" suite below asserts the close path, so
159+
// enable it here. The opt-in default itself is covered by the
160+
// dedicated "auto-close settings decision table" suite.
161+
autoCloseZooOpenedFiles: true,
157162
}),
158163
}),
159164
},
@@ -1711,7 +1716,24 @@ describe("DiffViewProvider", () => {
17111716
expect(vscode.window.showTextDocument).toHaveBeenCalled()
17121717
})
17131718

1714-
it("transient tab is closed when autoCloseZooOpenedFiles is true (default)", async () => {
1719+
it("transient tab is kept by default when autoCloseZooOpenedFiles is unset (opt-in)", async () => {
1720+
// Empty state -> autoCloseZooOpenedFiles is undefined and falls back to the
1721+
// centralized default (false), so an untouched transient tab is kept.
1722+
const provider = setupProvider({})
1723+
const closeFileTab = vi.fn().mockResolvedValue(undefined)
1724+
;(provider as any).closeFileTab = closeFileTab
1725+
;(provider as any).documentWasOpen = false
1726+
;(provider as any).userTouchedDocument = false
1727+
;(provider as any).userTouchedDiffEditor = false
1728+
vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any)
1729+
1730+
await provider.saveChanges(false)
1731+
1732+
expect(closeFileTab).not.toHaveBeenCalled()
1733+
expect(vscode.window.showTextDocument).toHaveBeenCalled()
1734+
})
1735+
1736+
it("transient tab is closed when autoCloseZooOpenedFiles is true", async () => {
17151737
const provider = setupProvider({ autoCloseZooOpenedFiles: true })
17161738
const closeFileTab = vi.fn().mockResolvedValue(undefined)
17171739
;(provider as any).closeFileTab = closeFileTab
@@ -1739,7 +1761,12 @@ describe("DiffViewProvider", () => {
17391761
})
17401762

17411763
it("touched tab is closed when autoCloseZooOpenedFilesAfterUserEdited is true", async () => {
1742-
const provider = setupProvider({ autoCloseZooOpenedFilesAfterUserEdited: true })
1764+
// The after-edit override only closes when the base auto-close is also
1765+
// enabled, so set both (the base default is now opt-in/false).
1766+
const provider = setupProvider({
1767+
autoCloseZooOpenedFiles: true,
1768+
autoCloseZooOpenedFilesAfterUserEdited: true,
1769+
})
17431770
const closeFileTab = vi.fn().mockResolvedValue(undefined)
17441771
;(provider as any).closeFileTab = closeFileTab
17451772
;(provider as any).documentWasOpen = false
@@ -1807,18 +1834,21 @@ describe("DiffViewProvider", () => {
18071834
expect(vscode.window.showTextDocument).toHaveBeenCalled()
18081835
})
18091836

1810-
it("defaults preserve existing behavior when all settings are unset", async () => {
1811-
// No auto-close settings in state: transient tab should be closed (existing default).
1837+
it("defaults keep the transient tab open when all settings are unset", async () => {
1838+
// No auto-close settings in state: auto-closing is opt-in, so an
1839+
// untouched transient tab is kept and re-shown (long-standing behavior).
18121840
const provider = setupProvider({})
18131841
const closeFileTab = vi.fn().mockResolvedValue(undefined)
18141842
;(provider as any).closeFileTab = closeFileTab
18151843
;(provider as any).documentWasOpen = false
18161844
;(provider as any).userTouchedDocument = false
18171845
;(provider as any).userTouchedDiffEditor = false
1846+
vi.mocked(vscode.window.showTextDocument).mockResolvedValue({ revealRange: vi.fn() } as any)
18181847

18191848
await provider.saveChanges(false)
18201849

1821-
expect(closeFileTab).toHaveBeenCalledWith(mockTargetPath)
1850+
expect(closeFileTab).not.toHaveBeenCalled()
1851+
expect(vscode.window.showTextDocument).toHaveBeenCalled()
18221852
})
18231853
})
18241854
})

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ import {
3535
type ProviderSettings,
3636
type ExperimentId,
3737
type TelemetrySetting,
38+
DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES,
39+
DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED,
40+
DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES,
3841
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
3942
ImageGenerationProvider,
4043
} from "@roo-code/types"
@@ -425,9 +428,10 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
425428
includeCurrentTime: includeCurrentTime ?? true,
426429
includeCurrentCost: includeCurrentCost ?? true,
427430
maxGitStatusFiles: maxGitStatusFiles ?? 0,
428-
autoCloseZooOpenedFiles: autoCloseZooOpenedFiles ?? true,
429-
autoCloseZooOpenedFilesAfterUserEdited: autoCloseZooOpenedFilesAfterUserEdited ?? false,
430-
autoCloseZooOpenedNewFiles: autoCloseZooOpenedNewFiles ?? false,
431+
autoCloseZooOpenedFiles: autoCloseZooOpenedFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES,
432+
autoCloseZooOpenedFilesAfterUserEdited:
433+
autoCloseZooOpenedFilesAfterUserEdited ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED,
434+
autoCloseZooOpenedNewFiles: autoCloseZooOpenedNewFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES,
431435
profileThresholds,
432436
imageGenerationProvider,
433437
openRouterImageApiKey,

webview-ui/src/components/settings/UISettings.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import { HTMLAttributes, useMemo } from "react"
22
import { useAppTranslation } from "@/i18n/TranslationContext"
33
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
44
import { telemetryClient } from "@/utils/TelemetryClient"
5+
import {
6+
DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES,
7+
DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED,
8+
DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES,
9+
} from "@roo-code/types"
510

611
import { SetCachedStateField } from "./types"
712
import { SectionHeader } from "./SectionHeader"
@@ -160,7 +165,7 @@ export const UISettings = ({
160165
label={t("settings:ui.autoCloseZooOpenedFiles.label")}>
161166
<div className="flex flex-col gap-1">
162167
<VSCodeCheckbox
163-
checked={autoCloseZooOpenedFiles ?? true}
168+
checked={autoCloseZooOpenedFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES}
164169
onChange={(e: any) => setCachedStateField("autoCloseZooOpenedFiles", e.target.checked)}
165170
data-testid="auto-close-zoo-opened-files-checkbox">
166171
<span className="font-medium">{t("settings:ui.autoCloseZooOpenedFiles.label")}</span>
@@ -178,7 +183,10 @@ export const UISettings = ({
178183
label={t("settings:ui.autoCloseZooOpenedFilesAfterUserEdited.label")}>
179184
<div className="flex flex-col gap-1">
180185
<VSCodeCheckbox
181-
checked={autoCloseZooOpenedFilesAfterUserEdited ?? false}
186+
checked={
187+
autoCloseZooOpenedFilesAfterUserEdited ??
188+
DEFAULT_AUTO_CLOSE_ZOO_OPENED_FILES_AFTER_USER_EDITED
189+
}
182190
onChange={(e: any) =>
183191
setCachedStateField("autoCloseZooOpenedFilesAfterUserEdited", e.target.checked)
184192
}
@@ -200,7 +208,7 @@ export const UISettings = ({
200208
label={t("settings:ui.autoCloseZooOpenedNewFiles.label")}>
201209
<div className="flex flex-col gap-1">
202210
<VSCodeCheckbox
203-
checked={autoCloseZooOpenedNewFiles ?? false}
211+
checked={autoCloseZooOpenedNewFiles ?? DEFAULT_AUTO_CLOSE_ZOO_OPENED_NEW_FILES}
204212
onChange={(e: any) =>
205213
setCachedStateField("autoCloseZooOpenedNewFiles", e.target.checked)
206214
}

webview-ui/src/components/settings/__tests__/UISettings.spec.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,14 @@ describe("UISettings", () => {
120120
expect(checkbox.checked).toBe(false)
121121
})
122122

123+
it("autoCloseZooOpenedFiles checkbox defaults to unchecked when prop is unset", () => {
124+
// Omitting the prop simulates the opt-in default (false). A regression that
125+
// flips the fallback back to `?? true` would make this checkbox checked.
126+
const { getByTestId } = render(<UISettings {...defaultProps} />)
127+
const checkbox = getByTestId("auto-close-zoo-opened-files-checkbox") as HTMLInputElement
128+
expect(checkbox.checked).toBe(false)
129+
})
130+
123131
it("calls setCachedStateField with autoCloseZooOpenedFiles when toggled", async () => {
124132
const setCachedStateField = vi.fn()
125133
const { getByTestId } = render(

0 commit comments

Comments
 (0)