fix: correct tab closing logic for autoCloseRooTabs setting

- Fixed logic to only close the edited file when autoCloseRooTabs is true but autoCloseAllRooTabs is false
- Updated tests to properly mock vscode.window.tabGroups.close() method
- Added isDirty property to test tab mocks
This commit is contained in:
Roo Code 2025-07-21 14:40:08 +00:00
parent 8722906284
commit d7d9baded4
3 changed files with 56 additions and 55 deletions

View file

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

View file

@ -77,8 +77,18 @@ export class PostEditBehaviorUtils {
if (tab.input instanceof vscode.TabInputText) {
const tabPath = tab.input.uri.fsPath
// Skip the currently edited file to avoid closing it immediately
if (editedFilePath && arePathsEqual(tabPath, editedFilePath)) {
// 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
}
@ -91,16 +101,6 @@ export class PostEditBehaviorUtils {
}
}
}
// If only autoCloseRooTabs is enabled, close tabs that Roo opened (not pre-existing)
if (autoCloseRooTabs && !autoCloseAllRooTabs) {
// This requires the tab to be in our tracked set
for (const trackedPath of rooOpenedTabs) {
if (arePathsEqual(tabPath, trackedPath)) {
return true
}
}
}
}
return false
@ -115,32 +115,32 @@ export class PostEditBehaviorUtils {
preEditActiveEditor: vscode.TextEditor | undefined,
editedFilePath?: string,
): Promise<void> {
// 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(),
)
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
if (stillExists) {
await vscode.window.showTextDocument(preEditActiveEditor.document, {
preserveFocus: false,
preview: false,
})
return
}
}
}
// Otherwise, try to focus on the edited file
if (editedFilePath) {
try {
// 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 to edited file: ${err}`)
}
} catch (err) {
// File might not exist or be accessible
console.debug(`Could not restore focus: ${err}`)
}
}
}

View file

@ -9,6 +9,7 @@ vi.mock("vscode", () => ({
window: {
tabGroups: {
all: [],
close: vi.fn(),
},
showTextDocument: vi.fn(),
createTextEditorDecorationType: vi.fn(() => ({
@ -51,6 +52,7 @@ describe("PostEditBehaviorUtils", () => {
// Reset mocks
vi.mocked(vscode.window).tabGroups = {
all: mockTabGroups,
close: vi.fn().mockResolvedValue(true),
} as any
vi.mocked(vscode.window).showTextDocument = mockShowTextDocument
vi.mocked(vscode.window).visibleTextEditors = mockVisibleTextEditors
@ -68,11 +70,11 @@ describe("PostEditBehaviorUtils", () => {
tabs: [
{
input: { uri: { fsPath: "/path/to/file1.ts" } },
close: vi.fn(),
isDirty: false,
},
{
input: { uri: { fsPath: "/path/to/file2.ts" } },
close: vi.fn(),
isDirty: false,
},
],
}
@ -87,8 +89,7 @@ describe("PostEditBehaviorUtils", () => {
)
// Assert
expect(mockTabGroup.tabs[0].close).not.toHaveBeenCalled()
expect(mockTabGroup.tabs[1].close).not.toHaveBeenCalled()
expect(vi.mocked(vscode.window).tabGroups.close).not.toHaveBeenCalled()
})
it("should close only the edited file tab when autoCloseRooTabs is true and autoCloseAllRooTabs is false", async () => {
@ -99,11 +100,11 @@ describe("PostEditBehaviorUtils", () => {
tabs: [
{
input: new (vi.mocked(vscode).TabInputText)(vscode.Uri.file("/path/to/file1.ts")),
close: vi.fn().mockResolvedValue(true),
isDirty: false,
},
{
input: new (vi.mocked(vscode).TabInputText)(vscode.Uri.file("/path/to/file2.ts")),
close: vi.fn().mockResolvedValue(true),
isDirty: false,
},
],
}
@ -118,8 +119,8 @@ describe("PostEditBehaviorUtils", () => {
)
// Assert
expect(mockTabGroup.tabs[0].close).toHaveBeenCalled()
expect(mockTabGroup.tabs[1].close).not.toHaveBeenCalled()
expect(vi.mocked(vscode.window).tabGroups.close).toHaveBeenCalledWith(mockTabGroup.tabs[0])
expect(vi.mocked(vscode.window).tabGroups.close).not.toHaveBeenCalledWith(mockTabGroup.tabs[1])
})
it("should close all Roo-opened tabs when autoCloseAllRooTabs is true", async () => {
@ -130,15 +131,15 @@ describe("PostEditBehaviorUtils", () => {
tabs: [
{
input: new (vi.mocked(vscode).TabInputText)(vscode.Uri.file("/path/to/file1.ts")),
close: vi.fn().mockResolvedValue(true),
isDirty: false,
},
{
input: new (vi.mocked(vscode).TabInputText)(vscode.Uri.file("/path/to/file2.ts")),
close: vi.fn().mockResolvedValue(true),
isDirty: false,
},
{
input: new (vi.mocked(vscode).TabInputText)(vscode.Uri.file("/path/to/file3.ts")),
close: vi.fn().mockResolvedValue(true),
isDirty: false,
},
],
}
@ -153,9 +154,9 @@ describe("PostEditBehaviorUtils", () => {
)
// Assert
expect(mockTabGroup.tabs[0].close).toHaveBeenCalled()
expect(mockTabGroup.tabs[1].close).toHaveBeenCalled()
expect(mockTabGroup.tabs[2].close).not.toHaveBeenCalled() // file3 was not opened by Roo
expect(vi.mocked(vscode.window).tabGroups.close).toHaveBeenCalledWith(mockTabGroup.tabs[0])
expect(vi.mocked(vscode.window).tabGroups.close).toHaveBeenCalledWith(mockTabGroup.tabs[1])
expect(vi.mocked(vscode.window).tabGroups.close).not.toHaveBeenCalledWith(mockTabGroup.tabs[2]) // file3 was not opened by Roo
})
it("should not close tabs that were not opened by Roo", async () => {
@ -166,11 +167,11 @@ describe("PostEditBehaviorUtils", () => {
tabs: [
{
input: new (vi.mocked(vscode).TabInputText)(vscode.Uri.file("/path/to/file1.ts")),
close: vi.fn().mockResolvedValue(true),
isDirty: false,
},
{
input: new (vi.mocked(vscode).TabInputText)(vscode.Uri.file("/path/to/file2.ts")), // Not in rooOpenedTabs
close: vi.fn().mockResolvedValue(true),
isDirty: false,
},
],
}
@ -185,8 +186,8 @@ describe("PostEditBehaviorUtils", () => {
)
// Assert
expect(mockTabGroup.tabs[0].close).toHaveBeenCalled()
expect(mockTabGroup.tabs[1].close).not.toHaveBeenCalled()
expect(vi.mocked(vscode.window).tabGroups.close).toHaveBeenCalledWith(mockTabGroup.tabs[0])
expect(vi.mocked(vscode.window).tabGroups.close).not.toHaveBeenCalledWith(mockTabGroup.tabs[1])
})
it("should handle tabs without URI input gracefully", async () => {
@ -196,15 +197,15 @@ describe("PostEditBehaviorUtils", () => {
tabs: [
{
input: new (vi.mocked(vscode).TabInputText)(vscode.Uri.file("/path/to/file1.ts")),
close: vi.fn().mockResolvedValue(true),
isDirty: false,
},
{
input: {}, // No URI
close: vi.fn().mockResolvedValue(true),
isDirty: false,
},
{
input: null, // Null input
close: vi.fn().mockResolvedValue(true),
isDirty: false,
},
],
}
@ -219,9 +220,9 @@ describe("PostEditBehaviorUtils", () => {
)
// Assert
expect(mockTabGroup.tabs[0].close).toHaveBeenCalled()
expect(mockTabGroup.tabs[1].close).not.toHaveBeenCalled()
expect(mockTabGroup.tabs[2].close).not.toHaveBeenCalled()
expect(vi.mocked(vscode.window).tabGroups.close).toHaveBeenCalledWith(mockTabGroup.tabs[0])
expect(vi.mocked(vscode.window).tabGroups.close).not.toHaveBeenCalledWith(mockTabGroup.tabs[1])
expect(vi.mocked(vscode.window).tabGroups.close).not.toHaveBeenCalledWith(mockTabGroup.tabs[2])
})
})