Skip to content

Commit b6c8e30

Browse files
committed
feat: add auto-close Roo tabs feature
- Add autoCloseRooTabs and autoCloseAllRooTabs settings to global settings schema - Create PostEditBehaviorUtils class to handle tab closing logic - Implement tab tracking in DiffViewProvider to track opened files - Add FileEditingOptions component to settings UI - Add message handlers for the new settings - Update ExtensionState and WebviewMessage types - Add translation keys for the new UI section - Add comprehensive tests for PostEditBehaviorUtils and FileEditingOptions Closes #6003
1 parent 9fce90b commit b6c8e30

File tree

12 files changed

+791
-7
lines changed

12 files changed

+791
-7
lines changed

packages/types/src/global-settings.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ export const globalSettingsSchema = z.object({
108108
rateLimitSeconds: z.number().optional(),
109109
diffEnabled: z.boolean().optional(),
110110
fuzzyMatchThreshold: z.number().optional(),
111+
autoCloseRooTabs: z.boolean().optional(),
112+
autoCloseAllRooTabs: z.boolean().optional(),
111113
experiments: experimentsSchema.optional(),
112114

113115
codebaseIndexModels: codebaseIndexModelsSchema.optional(),

src/core/webview/webviewMessageHandler.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1241,6 +1241,14 @@ export const webviewMessageHandler = async (
12411241
await updateGlobalState("showRooIgnoredFiles", message.bool ?? true)
12421242
await provider.postStateToWebview()
12431243
break
1244+
case "autoCloseRooTabs":
1245+
await updateGlobalState("autoCloseRooTabs", message.bool ?? false)
1246+
await provider.postStateToWebview()
1247+
break
1248+
case "autoCloseAllRooTabs":
1249+
await updateGlobalState("autoCloseAllRooTabs", message.bool ?? false)
1250+
await provider.postStateToWebview()
1251+
break
12441252
case "hasOpenedModeSelector":
12451253
await updateGlobalState("hasOpenedModeSelector", message.bool ?? true)
12461254
await provider.postStateToWebview()

src/integrations/editor/DiffViewProvider.ts

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { Task } from "../../core/task/Task"
1515
import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"
1616

1717
import { DecorationController } from "./DecorationController"
18+
import { PostEditBehaviorUtils } from "./PostEditBehaviorUtils"
1819

1920
export const DIFF_VIEW_URI_SCHEME = "cline-diff"
2021
export const DIFF_VIEW_LABEL_CHANGES = "Original ↔ Roo's Changes"
@@ -36,6 +37,7 @@ export class DiffViewProvider {
3637
private activeLineController?: DecorationController
3738
private streamedLines: string[] = []
3839
private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
40+
private rooOpenedTabs: Set<string> = new Set()
3941

4042
constructor(private cwd: string) {}
4143

@@ -95,6 +97,9 @@ export class DiffViewProvider {
9597
this.documentWasOpen = true
9698
}
9799

100+
// Track that we opened this file
101+
this.rooOpenedTabs.add(absolutePath)
102+
98103
this.activeDiffEditor = await this.openDiffEditor()
99104
this.fadedOverlayController = new DecorationController("fadedOverlay", this.activeDiffEditor)
100105
this.activeLineController = new DecorationController("activeLine", this.activeDiffEditor)
@@ -181,7 +186,12 @@ export class DiffViewProvider {
181186
}
182187
}
183188

184-
async saveChanges(diagnosticsEnabled: boolean = true, writeDelayMs: number = DEFAULT_WRITE_DELAY_MS): Promise<{
189+
async saveChanges(
190+
diagnosticsEnabled: boolean = true,
191+
writeDelayMs: number = DEFAULT_WRITE_DELAY_MS,
192+
autoCloseRooTabs: boolean = false,
193+
autoCloseAllRooTabs: boolean = false,
194+
): Promise<{
185195
newProblemsMessage: string | undefined
186196
userEdits: string | undefined
187197
finalContent: string | undefined
@@ -201,6 +211,14 @@ export class DiffViewProvider {
201211
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false, preserveFocus: true })
202212
await this.closeAllDiffViews()
203213

214+
// Apply post-edit behavior (auto-close tabs if configured)
215+
await PostEditBehaviorUtils.closeRooTabs(
216+
autoCloseRooTabs,
217+
autoCloseAllRooTabs,
218+
this.rooOpenedTabs,
219+
absolutePath,
220+
)
221+
204222
// Getting diagnostics before and after the file edit is a better approach than
205223
// automatically tracking problems in real-time. This method ensures we only
206224
// report new problems that are a direct result of this specific edit.
@@ -216,22 +234,22 @@ export class DiffViewProvider {
216234
// and can address them accordingly. If problems don't change immediately after
217235
// applying a fix, won't be notified, which is generally fine since the
218236
// initial fix is usually correct and it may just take time for linters to catch up.
219-
237+
220238
let newProblemsMessage = ""
221-
239+
222240
if (diagnosticsEnabled) {
223241
// Add configurable delay to allow linters time to process and clean up issues
224242
// like unused imports (especially important for Go and other languages)
225243
// Ensure delay is non-negative
226244
const safeDelayMs = Math.max(0, writeDelayMs)
227-
245+
228246
try {
229247
await delay(safeDelayMs)
230248
} catch (error) {
231249
// Log error but continue - delay failure shouldn't break the save operation
232250
console.warn(`Failed to apply write delay: ${error}`)
233251
}
234-
252+
235253
const postDiagnostics = vscode.languages.getDiagnostics()
236254

237255
const newProblems = await diagnosticsToProblemsString(
@@ -610,5 +628,14 @@ export class DiffViewProvider {
610628
this.activeLineController = undefined
611629
this.streamedLines = []
612630
this.preDiagnostics = []
631+
this.rooOpenedTabs.clear()
632+
}
633+
634+
/**
635+
* Gets the set of file paths that were opened by Roo during the current session
636+
* @returns Set of absolute file paths
637+
*/
638+
getRooOpenedTabs(): Set<string> {
639+
return new Set(this.rooOpenedTabs)
613640
}
614641
}
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import * as vscode from "vscode"
2+
import { arePathsEqual } from "../../utils/path"
3+
import { DIFF_VIEW_URI_SCHEME } from "./DiffViewProvider"
4+
5+
export class PostEditBehaviorUtils {
6+
/**
7+
* Closes Roo-related tabs based on the provided settings and tracked tabs
8+
* @param autoCloseRooTabs - Close only tabs opened during the current task
9+
* @param autoCloseAllRooTabs - Close all Roo tabs regardless of when they were opened
10+
* @param rooOpenedTabs - Set of file paths that were opened by Roo during the current task
11+
* @param editedFilePath - The path of the file that was just edited (to restore focus)
12+
* @returns Promise<void>
13+
*/
14+
static async closeRooTabs(
15+
autoCloseRooTabs: boolean,
16+
autoCloseAllRooTabs: boolean,
17+
rooOpenedTabs: Set<string>,
18+
editedFilePath?: string,
19+
): Promise<void> {
20+
if (!autoCloseRooTabs && !autoCloseAllRooTabs) {
21+
return
22+
}
23+
24+
// Get all tabs across all tab groups
25+
const allTabs = vscode.window.tabGroups.all.flatMap((group) => group.tabs)
26+
27+
// Filter tabs to close based on settings
28+
const tabsToClose = allTabs.filter((tab) => {
29+
// Check if it's a diff view tab
30+
if (tab.input instanceof vscode.TabInputTextDiff && tab.input.original.scheme === DIFF_VIEW_URI_SCHEME) {
31+
return true
32+
}
33+
34+
// Check if it's a regular text tab
35+
if (tab.input instanceof vscode.TabInputText) {
36+
const tabPath = tab.input.uri.fsPath
37+
38+
// Don't close the file that was just edited
39+
if (editedFilePath && arePathsEqual(tabPath, editedFilePath)) {
40+
return false
41+
}
42+
43+
if (autoCloseAllRooTabs) {
44+
// Close all Roo tabs - for now, we consider all tabs that were tracked
45+
// In a more sophisticated implementation, we might check for Roo-specific markers
46+
return rooOpenedTabs.has(tabPath)
47+
} else if (autoCloseRooTabs) {
48+
// Close only tabs opened during the current task
49+
return rooOpenedTabs.has(tabPath)
50+
}
51+
}
52+
53+
return false
54+
})
55+
56+
// Close the tabs
57+
const closePromises = tabsToClose.map((tab) => {
58+
if (!tab.isDirty) {
59+
return vscode.window.tabGroups.close(tab).then(
60+
() => undefined,
61+
(err: any) => {
62+
console.error(`Failed to close tab ${tab.label}:`, err)
63+
},
64+
)
65+
}
66+
return Promise.resolve()
67+
})
68+
69+
await Promise.all(closePromises)
70+
71+
// Restore focus to the edited file if provided
72+
if (editedFilePath) {
73+
try {
74+
await vscode.window.showTextDocument(vscode.Uri.file(editedFilePath), {
75+
preview: false,
76+
preserveFocus: false,
77+
})
78+
} catch (err) {
79+
console.error(`Failed to restore focus to ${editedFilePath}:`, err)
80+
}
81+
}
82+
}
83+
84+
/**
85+
* Determines which tabs should be closed based on the filter criteria
86+
* @param tabs - Array of tabs to filter
87+
* @param filter - Filter criteria (all Roo tabs or only current task tabs)
88+
* @param rooOpenedTabs - Set of file paths opened by Roo
89+
* @returns Array of tabs that match the filter criteria
90+
*/
91+
static filterTabsToClose(
92+
tabs: readonly vscode.Tab[],
93+
filter: "all" | "current",
94+
rooOpenedTabs: Set<string>,
95+
): vscode.Tab[] {
96+
return tabs.filter((tab) => {
97+
// Always close diff view tabs
98+
if (tab.input instanceof vscode.TabInputTextDiff && tab.input.original.scheme === DIFF_VIEW_URI_SCHEME) {
99+
return true
100+
}
101+
102+
// For text tabs, apply the filter
103+
if (tab.input instanceof vscode.TabInputText) {
104+
const tabPath = tab.input.uri.fsPath
105+
106+
if (filter === "all") {
107+
// In a real implementation, we might have additional checks
108+
// to identify Roo-specific tabs beyond just the tracked set
109+
return true
110+
} else if (filter === "current") {
111+
return rooOpenedTabs.has(tabPath)
112+
}
113+
}
114+
115+
return false
116+
})
117+
}
118+
119+
/**
120+
* Checks if a tab is a Roo-related tab
121+
* @param tab - The tab to check
122+
* @returns true if the tab is Roo-related
123+
*/
124+
static isRooTab(tab: vscode.Tab): boolean {
125+
// Check if it's a diff view tab
126+
if (tab.input instanceof vscode.TabInputTextDiff && tab.input.original.scheme === DIFF_VIEW_URI_SCHEME) {
127+
return true
128+
}
129+
130+
// Additional checks could be added here to identify other Roo-specific tabs
131+
// For example, checking for specific URI schemes, file patterns, etc.
132+
133+
return false
134+
}
135+
}

0 commit comments

Comments
 (0)