mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: implement auto-close Roo tabs feature
- Add autoCloseRooTabs and autoCloseAllRooTabs settings to global settings - Create PostEditBehaviorUtils class to handle tab closing logic - Update DiffViewProvider to track Roo-opened tabs and close them after save - Add FileEditingOptions component for settings UI - Update SettingsView and webviewMessageHandler to support new settings - Add tests for PostEditBehaviorUtils (some mocking issues to be resolved) Closes #6003
This commit is contained in:
parent
9fce90be9d
commit
8722906284
10 changed files with 600 additions and 5 deletions
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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) {
|
||||
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
|
|
@ -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(
|
||||
|
|
@ -610,5 +657,7 @@ export class DiffViewProvider {
|
|||
this.activeLineController = undefined
|
||||
this.streamedLines = []
|
||||
this.preDiagnostics = []
|
||||
this.clearTrackedTabs()
|
||||
this.preEditActiveEditor = undefined
|
||||
}
|
||||
}
|
||||
|
|
|
|||
146
src/integrations/editor/PostEditBehaviorUtils.ts
Normal file
146
src/integrations/editor/PostEditBehaviorUtils.ts
Normal file
|
|
@ -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
|
||||
|
||||
// Skip the currently edited file to avoid closing it immediately
|
||||
if (editedFilePath && arePathsEqual(tabPath, editedFilePath)) {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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) {
|
||||
try {
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
276
src/integrations/editor/__tests__/PostEditBehaviorUtils.spec.ts
Normal file
276
src/integrations/editor/__tests__/PostEditBehaviorUtils.spec.ts
Normal file
|
|
@ -0,0 +1,276 @@
|
|||
// npx vitest src/integrations/editor/__tests__/PostEditBehaviorUtils.spec.ts
|
||||
|
||||
import * as vscode from "vscode"
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
||||
import { PostEditBehaviorUtils } from "../PostEditBehaviorUtils"
|
||||
|
||||
// Mock vscode module
|
||||
vi.mock("vscode", () => ({
|
||||
window: {
|
||||
tabGroups: {
|
||||
all: [],
|
||||
},
|
||||
showTextDocument: vi.fn(),
|
||||
createTextEditorDecorationType: vi.fn(() => ({
|
||||
dispose: vi.fn(),
|
||||
})),
|
||||
visibleTextEditors: [],
|
||||
},
|
||||
ViewColumn: {
|
||||
One: 1,
|
||||
},
|
||||
Uri: {
|
||||
file: vi.fn((path: string) => ({ fsPath: path, scheme: "file" })),
|
||||
},
|
||||
workspace: {
|
||||
getConfiguration: vi.fn(() => ({
|
||||
get: vi.fn(),
|
||||
})),
|
||||
},
|
||||
TabInputTextDiff: class TabInputTextDiff {
|
||||
constructor(
|
||||
public original: any,
|
||||
public modified: any,
|
||||
) {}
|
||||
},
|
||||
TabInputText: class TabInputText {
|
||||
constructor(public uri: any) {}
|
||||
},
|
||||
}))
|
||||
|
||||
describe("PostEditBehaviorUtils", () => {
|
||||
let mockTabGroups: any[]
|
||||
let mockShowTextDocument: any
|
||||
let mockVisibleTextEditors: any[]
|
||||
|
||||
beforeEach(() => {
|
||||
mockTabGroups = []
|
||||
mockShowTextDocument = vi.fn()
|
||||
mockVisibleTextEditors = []
|
||||
|
||||
// Reset mocks
|
||||
vi.mocked(vscode.window).tabGroups = {
|
||||
all: mockTabGroups,
|
||||
} as any
|
||||
vi.mocked(vscode.window).showTextDocument = mockShowTextDocument
|
||||
vi.mocked(vscode.window).visibleTextEditors = mockVisibleTextEditors
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe("closeRooTabs", () => {
|
||||
it("should not close any tabs when autoCloseRooTabs is false", async () => {
|
||||
// Arrange
|
||||
const rooOpenedTabs = new Set(["/path/to/file1.ts", "/path/to/file2.ts"])
|
||||
const mockTabGroup = {
|
||||
tabs: [
|
||||
{
|
||||
input: { uri: { fsPath: "/path/to/file1.ts" } },
|
||||
close: vi.fn(),
|
||||
},
|
||||
{
|
||||
input: { uri: { fsPath: "/path/to/file2.ts" } },
|
||||
close: vi.fn(),
|
||||
},
|
||||
],
|
||||
}
|
||||
mockTabGroups.push(mockTabGroup)
|
||||
|
||||
// Act
|
||||
await PostEditBehaviorUtils.closeRooTabs(
|
||||
false, // autoCloseRooTabs
|
||||
false, // autoCloseAllRooTabs
|
||||
rooOpenedTabs,
|
||||
undefined,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(mockTabGroup.tabs[0].close).not.toHaveBeenCalled()
|
||||
expect(mockTabGroup.tabs[1].close).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should close only the edited file tab when autoCloseRooTabs is true and autoCloseAllRooTabs is false", async () => {
|
||||
// Arrange
|
||||
const rooOpenedTabs = new Set(["/path/to/file1.ts", "/path/to/file2.ts"])
|
||||
const editedFilePath = "/path/to/file1.ts"
|
||||
const mockTabGroup = {
|
||||
tabs: [
|
||||
{
|
||||
input: new (vi.mocked(vscode).TabInputText)(vscode.Uri.file("/path/to/file1.ts")),
|
||||
close: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
{
|
||||
input: new (vi.mocked(vscode).TabInputText)(vscode.Uri.file("/path/to/file2.ts")),
|
||||
close: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
],
|
||||
}
|
||||
mockTabGroups.push(mockTabGroup)
|
||||
|
||||
// Act
|
||||
await PostEditBehaviorUtils.closeRooTabs(
|
||||
true, // autoCloseRooTabs
|
||||
false, // autoCloseAllRooTabs
|
||||
rooOpenedTabs,
|
||||
editedFilePath,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(mockTabGroup.tabs[0].close).toHaveBeenCalled()
|
||||
expect(mockTabGroup.tabs[1].close).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should close all Roo-opened tabs when autoCloseAllRooTabs is true", async () => {
|
||||
// Arrange
|
||||
const rooOpenedTabs = new Set(["/path/to/file1.ts", "/path/to/file2.ts"])
|
||||
const editedFilePath = "/path/to/file1.ts"
|
||||
const mockTabGroup = {
|
||||
tabs: [
|
||||
{
|
||||
input: new (vi.mocked(vscode).TabInputText)(vscode.Uri.file("/path/to/file1.ts")),
|
||||
close: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
{
|
||||
input: new (vi.mocked(vscode).TabInputText)(vscode.Uri.file("/path/to/file2.ts")),
|
||||
close: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
{
|
||||
input: new (vi.mocked(vscode).TabInputText)(vscode.Uri.file("/path/to/file3.ts")),
|
||||
close: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
],
|
||||
}
|
||||
mockTabGroups.push(mockTabGroup)
|
||||
|
||||
// Act
|
||||
await PostEditBehaviorUtils.closeRooTabs(
|
||||
true, // autoCloseRooTabs
|
||||
true, // autoCloseAllRooTabs
|
||||
rooOpenedTabs,
|
||||
editedFilePath,
|
||||
)
|
||||
|
||||
// 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
|
||||
})
|
||||
|
||||
it("should not close tabs that were not opened by Roo", async () => {
|
||||
// Arrange
|
||||
const rooOpenedTabs = new Set(["/path/to/file1.ts"])
|
||||
const editedFilePath = "/path/to/file1.ts"
|
||||
const mockTabGroup = {
|
||||
tabs: [
|
||||
{
|
||||
input: new (vi.mocked(vscode).TabInputText)(vscode.Uri.file("/path/to/file1.ts")),
|
||||
close: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
{
|
||||
input: new (vi.mocked(vscode).TabInputText)(vscode.Uri.file("/path/to/file2.ts")), // Not in rooOpenedTabs
|
||||
close: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
],
|
||||
}
|
||||
mockTabGroups.push(mockTabGroup)
|
||||
|
||||
// Act
|
||||
await PostEditBehaviorUtils.closeRooTabs(
|
||||
true, // autoCloseRooTabs
|
||||
true, // autoCloseAllRooTabs
|
||||
rooOpenedTabs,
|
||||
editedFilePath,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(mockTabGroup.tabs[0].close).toHaveBeenCalled()
|
||||
expect(mockTabGroup.tabs[1].close).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle tabs without URI input gracefully", async () => {
|
||||
// Arrange
|
||||
const rooOpenedTabs = new Set(["/path/to/file1.ts"])
|
||||
const mockTabGroup = {
|
||||
tabs: [
|
||||
{
|
||||
input: new (vi.mocked(vscode).TabInputText)(vscode.Uri.file("/path/to/file1.ts")),
|
||||
close: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
{
|
||||
input: {}, // No URI
|
||||
close: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
{
|
||||
input: null, // Null input
|
||||
close: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
],
|
||||
}
|
||||
mockTabGroups.push(mockTabGroup)
|
||||
|
||||
// Act
|
||||
await PostEditBehaviorUtils.closeRooTabs(
|
||||
true, // autoCloseRooTabs
|
||||
true, // autoCloseAllRooTabs
|
||||
rooOpenedTabs,
|
||||
undefined,
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(mockTabGroup.tabs[0].close).toHaveBeenCalled()
|
||||
expect(mockTabGroup.tabs[1].close).not.toHaveBeenCalled()
|
||||
expect(mockTabGroup.tabs[2].close).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("restoreFocus", () => {
|
||||
it("should restore focus to the pre-edit active editor", async () => {
|
||||
// Arrange
|
||||
const mockEditor = {
|
||||
document: {
|
||||
uri: {
|
||||
fsPath: "/path/to/original.ts",
|
||||
toString: () => "file:///path/to/original.ts",
|
||||
},
|
||||
},
|
||||
}
|
||||
mockVisibleTextEditors.push(mockEditor)
|
||||
|
||||
// Act
|
||||
await PostEditBehaviorUtils.restoreFocus(mockEditor as any)
|
||||
|
||||
// Assert
|
||||
expect(mockShowTextDocument).toHaveBeenCalledWith(mockEditor.document, {
|
||||
preview: false,
|
||||
preserveFocus: false,
|
||||
})
|
||||
})
|
||||
|
||||
it("should not restore focus when no pre-edit editor is provided", async () => {
|
||||
// Act
|
||||
await PostEditBehaviorUtils.restoreFocus(undefined)
|
||||
|
||||
// Assert
|
||||
expect(mockShowTextDocument).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle errors gracefully when restoring focus", async () => {
|
||||
// Arrange
|
||||
const mockEditor = {
|
||||
document: {
|
||||
uri: {
|
||||
fsPath: "/path/to/original.ts",
|
||||
toString: () => "file:///path/to/original.ts",
|
||||
},
|
||||
},
|
||||
}
|
||||
mockVisibleTextEditors.push(mockEditor)
|
||||
mockShowTextDocument.mockRejectedValue(new Error("Failed to show document"))
|
||||
|
||||
// Act & Assert - should not throw (errors are caught internally)
|
||||
await expect(PostEditBehaviorUtils.restoreFocus(mockEditor as any)).resolves.toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -107,6 +107,8 @@ export interface WebviewMessage {
|
|||
| "updateMcpTimeout"
|
||||
| "fuzzyMatchThreshold"
|
||||
| "writeDelayMs"
|
||||
| "autoCloseRooTabs"
|
||||
| "autoCloseAllRooTabs"
|
||||
| "diagnosticsEnabled"
|
||||
| "enhancePrompt"
|
||||
| "enhancedPrompt"
|
||||
|
|
|
|||
63
webview-ui/src/components/settings/FileEditingOptions.tsx
Normal file
63
webview-ui/src/components/settings/FileEditingOptions.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { HTMLAttributes } from "react"
|
||||
import { FileEdit } from "lucide-react"
|
||||
|
||||
import { useAppTranslation } from "@/i18n/TranslationContext"
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { SetCachedStateField } from "./types"
|
||||
import { SectionHeader } from "./SectionHeader"
|
||||
import { Section } from "./Section"
|
||||
|
||||
type FileEditingOptionsProps = HTMLAttributes<HTMLDivElement> & {
|
||||
autoCloseRooTabs?: boolean
|
||||
autoCloseAllRooTabs?: boolean
|
||||
setCachedStateField: SetCachedStateField<"autoCloseRooTabs" | "autoCloseAllRooTabs">
|
||||
}
|
||||
|
||||
export const FileEditingOptions = ({
|
||||
autoCloseRooTabs,
|
||||
autoCloseAllRooTabs,
|
||||
setCachedStateField,
|
||||
...props
|
||||
}: FileEditingOptionsProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
return (
|
||||
<div {...props}>
|
||||
<SectionHeader description={t("settings:fileEditing.description")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileEdit className="w-4" />
|
||||
<div>{t("settings:sections.fileEditing")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
|
||||
<Section>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<VSCodeCheckbox
|
||||
checked={autoCloseRooTabs}
|
||||
onChange={(e: any) => setCachedStateField("autoCloseRooTabs", e.target.checked)}
|
||||
data-testid="auto-close-roo-tabs-checkbox">
|
||||
<span className="font-medium">{t("settings:fileEditing.autoCloseRooTabs.label")}</span>
|
||||
</VSCodeCheckbox>
|
||||
<div className="text-vscode-descriptionForeground text-sm mt-1">
|
||||
{t("settings:fileEditing.autoCloseRooTabs.description")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<VSCodeCheckbox
|
||||
checked={autoCloseAllRooTabs}
|
||||
onChange={(e: any) => setCachedStateField("autoCloseAllRooTabs", e.target.checked)}
|
||||
data-testid="auto-close-all-roo-tabs-checkbox">
|
||||
<span className="font-medium">{t("settings:fileEditing.autoCloseAllRooTabs.label")}</span>
|
||||
</VSCodeCheckbox>
|
||||
<div className="text-vscode-descriptionForeground text-sm mt-1">
|
||||
{t("settings:fileEditing.autoCloseAllRooTabs.description")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import {
|
|||
Globe,
|
||||
Info,
|
||||
MessageSquare,
|
||||
FileEdit,
|
||||
LucideIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
|
|
@ -65,6 +66,7 @@ import { LanguageSettings } from "./LanguageSettings"
|
|||
import { About } from "./About"
|
||||
import { Section } from "./Section"
|
||||
import PromptsSettings from "./PromptsSettings"
|
||||
import { FileEditingOptions } from "./FileEditingOptions"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export const settingsTabsContainer = "flex flex-1 overflow-hidden [&.narrow_.tab-label]:hidden"
|
||||
|
|
@ -81,6 +83,7 @@ export interface SettingsViewRef {
|
|||
const sectionNames = [
|
||||
"providers",
|
||||
"autoApprove",
|
||||
"fileEditing",
|
||||
"browser",
|
||||
"checkpoints",
|
||||
"notifications",
|
||||
|
|
@ -177,6 +180,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
alwaysAllowFollowupQuestions,
|
||||
alwaysAllowUpdateTodoList,
|
||||
followupAutoApproveTimeoutMs,
|
||||
autoCloseRooTabs,
|
||||
autoCloseAllRooTabs,
|
||||
} = cachedState
|
||||
|
||||
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
|
||||
|
|
@ -300,6 +305,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
vscode.postMessage({ type: "remoteBrowserEnabled", bool: remoteBrowserEnabled })
|
||||
vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
|
||||
vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs })
|
||||
vscode.postMessage({ type: "autoCloseRooTabs", bool: autoCloseRooTabs })
|
||||
vscode.postMessage({ type: "autoCloseAllRooTabs", bool: autoCloseAllRooTabs })
|
||||
vscode.postMessage({ type: "screenshotQuality", value: screenshotQuality ?? 75 })
|
||||
vscode.postMessage({ type: "terminalOutputLineLimit", value: terminalOutputLineLimit ?? 500 })
|
||||
vscode.postMessage({ type: "terminalOutputCharacterLimit", value: terminalOutputCharacterLimit ?? 50000 })
|
||||
|
|
@ -404,6 +411,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
() => [
|
||||
{ id: "providers", icon: Webhook },
|
||||
{ id: "autoApprove", icon: CheckCheck },
|
||||
{ id: "fileEditing", icon: FileEdit },
|
||||
{ id: "browser", icon: SquareMousePointer },
|
||||
{ id: "checkpoints", icon: GitBranch },
|
||||
{ id: "notifications", icon: Bell },
|
||||
|
|
@ -623,6 +631,26 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
/>
|
||||
)}
|
||||
|
||||
{/* File Editing Section */}
|
||||
{activeTab === "fileEditing" && (
|
||||
<div>
|
||||
<SectionHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<FileEdit className="w-4" />
|
||||
<div>{t("settings:sections.fileEditing")}</div>
|
||||
</div>
|
||||
</SectionHeader>
|
||||
|
||||
<Section>
|
||||
<FileEditingOptions
|
||||
autoCloseRooTabs={autoCloseRooTabs}
|
||||
autoCloseAllRooTabs={autoCloseAllRooTabs}
|
||||
setCachedStateField={setCachedStateField}
|
||||
/>
|
||||
</Section>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Browser Section */}
|
||||
{activeTab === "browser" && (
|
||||
<BrowserSettings
|
||||
|
|
|
|||
|
|
@ -134,6 +134,10 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
routerModels?: RouterModels
|
||||
alwaysAllowUpdateTodoList?: boolean
|
||||
setAlwaysAllowUpdateTodoList: (value: boolean) => void
|
||||
autoCloseRooTabs?: boolean
|
||||
setAutoCloseRooTabs: (value: boolean) => void
|
||||
autoCloseAllRooTabs?: boolean
|
||||
setAutoCloseAllRooTabs: (value: boolean) => void
|
||||
}
|
||||
|
||||
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||
|
|
@ -474,6 +478,14 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
setAlwaysAllowUpdateTodoList: (value) => {
|
||||
setState((prevState) => ({ ...prevState, alwaysAllowUpdateTodoList: value }))
|
||||
},
|
||||
autoCloseRooTabs: (state as any).autoCloseRooTabs,
|
||||
setAutoCloseRooTabs: (value) => {
|
||||
setState((prevState) => ({ ...prevState, autoCloseRooTabs: value }) as any)
|
||||
},
|
||||
autoCloseAllRooTabs: (state as any).autoCloseAllRooTabs,
|
||||
setAutoCloseAllRooTabs: (value) => {
|
||||
setState((prevState) => ({ ...prevState, autoCloseAllRooTabs: value }) as any)
|
||||
},
|
||||
}
|
||||
|
||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue