Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export const globalSettingsSchema = z.object({
autoCondenseContextPercent: z.number().optional(),
maxConcurrentFileReads: z.number().optional(),

// File editing options
autoCloseRooTabs: z.boolean().optional(),
autoCloseAllRooTabs: z.boolean().optional(),

browserToolEnabled: z.boolean().optional(),
browserViewportSize: z.string().optional(),
screenshotQuality: z.number().optional(),
Expand Down
7 changes: 7 additions & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,13 @@ export class Task extends EventEmitter<ClineEvents> {
this.diffViewProvider = new DiffViewProvider(this.cwd)
this.enableCheckpoints = enableCheckpoints

// Set up auto-close settings for DiffViewProvider
provider.getState().then((state) => {
const autoCloseRooTabs = (state as any).autoCloseRooTabs ?? false
const autoCloseAllRooTabs = (state as any).autoCloseAllRooTabs ?? false
this.diffViewProvider.setAutoCloseSettings(autoCloseRooTabs, autoCloseAllRooTabs)
})

this.rootTask = rootTask
this.parentTask = parentTask
this.taskNumber = taskNumber
Expand Down
8 changes: 8 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,14 @@ export const webviewMessageHandler = async (
await updateGlobalState("writeDelayMs", message.value)
await provider.postStateToWebview()
break
case "autoCloseRooTabs":
await updateGlobalState("autoCloseRooTabs", message.bool ?? false)
await provider.postStateToWebview()
break
case "autoCloseAllRooTabs":
await updateGlobalState("autoCloseAllRooTabs", message.bool ?? false)
await provider.postStateToWebview()
break
case "diagnosticsEnabled":
await updateGlobalState("diagnosticsEnabled", message.bool ?? true)
await provider.postStateToWebview()
Expand Down
61 changes: 55 additions & 6 deletions src/integrations/editor/DiffViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Task } from "../../core/task/Task"
import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types"

import { DecorationController } from "./DecorationController"
import { PostEditBehaviorUtils } from "./PostEditBehaviorUtils"

export const DIFF_VIEW_URI_SCHEME = "cline-diff"
export const DIFF_VIEW_LABEL_CHANGES = "Original ↔ Roo's Changes"
Expand All @@ -36,15 +37,47 @@ export class DiffViewProvider {
private activeLineController?: DecorationController
private streamedLines: string[] = []
private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = []
private rooOpenedTabs: Set<string> = new Set()
private preEditActiveEditor?: vscode.TextEditor
private autoCloseRooTabs: boolean = false
private autoCloseAllRooTabs: boolean = false

constructor(private cwd: string) {}

/**
* Set the auto-close settings for this DiffViewProvider instance
*/
setAutoCloseSettings(autoCloseRooTabs: boolean, autoCloseAllRooTabs: boolean) {
this.autoCloseRooTabs = autoCloseRooTabs
this.autoCloseAllRooTabs = autoCloseAllRooTabs
}

/**
* Track a tab that Roo opened
*/
trackOpenedTab(filePath: string) {
this.rooOpenedTabs.add(filePath)
}

/**
* Clear all tracked tabs
*/
clearTrackedTabs() {
this.rooOpenedTabs.clear()
}

async open(relPath: string): Promise<void> {
this.relPath = relPath
const fileExists = this.editType === "modify"
const absolutePath = path.resolve(this.cwd, relPath)
this.isEditing = true

// Store the currently active editor before we start making changes
this.preEditActiveEditor = vscode.window.activeTextEditor

// Track that Roo is opening this file
this.trackOpenedTab(absolutePath)

// If the file is already open, ensure it's not dirty before getting its
// contents.
if (fileExists) {
Expand Down Expand Up @@ -181,7 +214,10 @@ export class DiffViewProvider {
}
}

async saveChanges(diagnosticsEnabled: boolean = true, writeDelayMs: number = DEFAULT_WRITE_DELAY_MS): Promise<{
async saveChanges(
diagnosticsEnabled: boolean = true,
writeDelayMs: number = DEFAULT_WRITE_DELAY_MS,
): Promise<{
newProblemsMessage: string | undefined
userEdits: string | undefined
finalContent: string | undefined
Expand All @@ -201,6 +237,17 @@ export class DiffViewProvider {
await vscode.window.showTextDocument(vscode.Uri.file(absolutePath), { preview: false, preserveFocus: true })
await this.closeAllDiffViews()

// Apply post-edit behaviors (tab closing)
await PostEditBehaviorUtils.closeRooTabs(
this.autoCloseRooTabs,
this.autoCloseAllRooTabs,
this.rooOpenedTabs,
absolutePath,
)

// Restore focus after closing tabs
await PostEditBehaviorUtils.restoreFocus(this.preEditActiveEditor, absolutePath)

// Getting diagnostics before and after the file edit is a better approach than
// automatically tracking problems in real-time. This method ensures we only
// report new problems that are a direct result of this specific edit.
Expand All @@ -216,22 +263,22 @@ export class DiffViewProvider {
// and can address them accordingly. If problems don't change immediately after
// applying a fix, won't be notified, which is generally fine since the
// initial fix is usually correct and it may just take time for linters to catch up.

let newProblemsMessage = ""

if (diagnosticsEnabled) {
// Add configurable delay to allow linters time to process and clean up issues
// like unused imports (especially important for Go and other languages)
// Ensure delay is non-negative
const safeDelayMs = Math.max(0, writeDelayMs)

try {
await delay(safeDelayMs)
} catch (error) {
// Log error but continue - delay failure shouldn't break the save operation
console.warn(`Failed to apply write delay: ${error}`)
}

const postDiagnostics = vscode.languages.getDiagnostics()

const newProblems = await diagnosticsToProblemsString(
Expand Down Expand Up @@ -426,7 +473,7 @@ export class DiffViewProvider {
.map((tab) =>
vscode.window.tabGroups.close(tab).then(
() => undefined,
(err) => {
(err: any) => {
console.error(`Failed to close diff tab ${tab.label}`, err)
},
),
Expand Down Expand Up @@ -610,5 +657,7 @@ export class DiffViewProvider {
this.activeLineController = undefined
this.streamedLines = []
this.preDiagnostics = []
this.clearTrackedTabs()
this.preEditActiveEditor = undefined
}
}
146 changes: 146 additions & 0 deletions src/integrations/editor/PostEditBehaviorUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import * as vscode from "vscode"
import { arePathsEqual } from "../../utils/path"
import { DIFF_VIEW_URI_SCHEME, DIFF_VIEW_LABEL_CHANGES } from "./DiffViewProvider"

export class PostEditBehaviorUtils {
/**
* Closes tabs based on the provided settings and tracked tabs
* @param autoCloseRooTabs - Close only tabs that were not open prior to the current task
* @param autoCloseAllRooTabs - Close all Roo tabs regardless of prior state
* @param rooOpenedTabs - Set of file paths that Roo opened during the current task
* @param editedFilePath - The path of the file that was just edited (optional)
* @returns Promise that resolves when all tabs are closed
*/
static async closeRooTabs(
autoCloseRooTabs: boolean,
autoCloseAllRooTabs: boolean,
rooOpenedTabs: Set<string>,
editedFilePath?: string,
): Promise<void> {
if (!autoCloseRooTabs && !autoCloseAllRooTabs) {
return
}

const tabsToClose: vscode.Tab[] = []

// Iterate through all tab groups and their tabs
for (const tabGroup of vscode.window.tabGroups.all) {
for (const tab of tabGroup.tabs) {
if (this.shouldCloseTab(tab, autoCloseRooTabs, autoCloseAllRooTabs, rooOpenedTabs, editedFilePath)) {
tabsToClose.push(tab)
}
}
}

// Close all identified tabs
if (tabsToClose.length > 0) {
await Promise.all(
tabsToClose.map(async (tab) => {
try {
await vscode.window.tabGroups.close(tab)
} catch (err) {
console.error(`Failed to close tab: ${err}`)
}
}),
)
}
}

/**
* Determines if a tab should be closed based on the settings and tab properties
*/
private static shouldCloseTab(
tab: vscode.Tab,
autoCloseRooTabs: boolean,
autoCloseAllRooTabs: boolean,
rooOpenedTabs: Set<string>,
editedFilePath?: string,
): boolean {
// Don't close dirty tabs
if (tab.isDirty) {
return false
}

// Check if this is a diff view tab
if (tab.input instanceof vscode.TabInputTextDiff) {
// Check if it's a Roo diff view by URI scheme
if (tab.input.original.scheme === DIFF_VIEW_URI_SCHEME) {
return autoCloseAllRooTabs || autoCloseRooTabs
}
// Also check by label for compatibility
if (tab.label.includes(DIFF_VIEW_LABEL_CHANGES)) {
return autoCloseAllRooTabs || autoCloseRooTabs
}
}

// Check if this is a regular text tab
if (tab.input instanceof vscode.TabInputText) {
const tabPath = tab.input.uri.fsPath

// If only autoCloseRooTabs is enabled (not autoCloseAllRooTabs),
// only close the edited file tab
if (autoCloseRooTabs && !autoCloseAllRooTabs) {
// Only close if this is the edited file AND it was opened by Roo
if (editedFilePath && arePathsEqual(tabPath, editedFilePath)) {
// Check if this file was opened by Roo
for (const trackedPath of rooOpenedTabs) {
if (arePathsEqual(tabPath, trackedPath)) {
return true
}
}
}
return false
}

// If autoCloseAllRooTabs is enabled, close any tab that was tracked by Roo
if (autoCloseAllRooTabs) {
// Check if this tab's path exists in our tracked set
for (const trackedPath of rooOpenedTabs) {
if (arePathsEqual(tabPath, trackedPath)) {
return true
}
}
}
}

return false
}

/**
* Restores focus to the appropriate editor after closing tabs
* @param preEditActiveEditor - The editor that was active before the edit operation
* @param editedFilePath - The path of the file that was edited
*/
static async restoreFocus(
preEditActiveEditor: vscode.TextEditor | undefined,
editedFilePath?: string,
): Promise<void> {
try {
// Try to restore focus to the pre-edit active editor if it still exists
if (preEditActiveEditor) {
const stillExists = vscode.window.visibleTextEditors.some(
(editor) => editor.document.uri.toString() === preEditActiveEditor.document.uri.toString(),
)

if (stillExists) {
await vscode.window.showTextDocument(preEditActiveEditor.document, {
preserveFocus: false,
preview: false,
})
return
}
}

// Otherwise, try to focus on the edited file
if (editedFilePath) {
await vscode.window.showTextDocument(vscode.Uri.file(editedFilePath), {
preserveFocus: false,
preview: false,
})
}
} catch (err) {
// File might not exist or be accessible
console.debug(`Could not restore focus: ${err}`)
}
}
}
Loading
Loading