diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index a30550dce16..7a74fa345a1 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -107,6 +107,7 @@ export const globalSettingsSchema = z.object({ rateLimitSeconds: z.number().optional(), diffEnabled: z.boolean().optional(), + openTabsInCorrectGroup: z.boolean().optional(), fuzzyMatchThreshold: z.number().optional(), experiments: experimentsSchema.optional(), diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 53b8ef5b87d..abdb5d99eaa 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -260,7 +260,7 @@ export class Task extends EventEmitter { this.consecutiveMistakeLimit = consecutiveMistakeLimit ?? DEFAULT_CONSECUTIVE_MISTAKE_LIMIT this.providerRef = new WeakRef(provider) this.globalStoragePath = provider.context.globalStorageUri.fsPath - this.diffViewProvider = new DiffViewProvider(this.cwd) + this.diffViewProvider = new DiffViewProvider(this.cwd, provider) this.enableCheckpoints = enableCheckpoints this.rootTask = rootTask diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 6231f081670..17c8329800c 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1726,6 +1726,7 @@ export class ClineProvider codebaseIndexSearchMinScore: stateValues.codebaseIndexConfig?.codebaseIndexSearchMinScore, }, profileThresholds: stateValues.profileThresholds ?? {}, + openTabsInCorrectGroup: stateValues.openTabsInCorrectGroup ?? false, } } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 780d40df891..02ecfe022a3 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -932,6 +932,11 @@ export const webviewMessageHandler = async ( await updateGlobalState("diffEnabled", diffEnabled) await provider.postStateToWebview() break + case "openTabsInCorrectGroup": + const openTabsInCorrectGroup = message.bool ?? false + await updateGlobalState("openTabsInCorrectGroup", openTabsInCorrectGroup) + await provider.postStateToWebview() + break case "enableCheckpoints": const enableCheckpoints = message.bool ?? true await updateGlobalState("enableCheckpoints", enableCheckpoints) diff --git a/src/integrations/editor/DiffViewProvider.ts b/src/integrations/editor/DiffViewProvider.ts index f4133029c99..3b866c8827c 100644 --- a/src/integrations/editor/DiffViewProvider.ts +++ b/src/integrations/editor/DiffViewProvider.ts @@ -13,6 +13,7 @@ import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics" import { ClineSayTool } from "../../shared/ExtensionMessage" import { Task } from "../../core/task/Task" import { DEFAULT_WRITE_DELAY_MS } from "@roo-code/types" +import { ClineProvider } from "../../core/webview/ClineProvider" import { DecorationController } from "./DecorationController" @@ -36,8 +37,16 @@ export class DiffViewProvider { private activeLineController?: DecorationController private streamedLines: string[] = [] private preDiagnostics: [vscode.Uri, vscode.Diagnostic[]][] = [] - - constructor(private cwd: string) {} + private providerRef?: WeakRef + + constructor( + private cwd: string, + provider?: ClineProvider, + ) { + if (provider) { + this.providerRef = new WeakRef(provider) + } + } async open(relPath: string): Promise { this.relPath = relPath @@ -181,7 +190,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 @@ -216,22 +228,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( @@ -461,6 +473,53 @@ export class DiffViewProvider { return editor } + // Determine the view column based on the openTabsInCorrectGroup setting + let targetViewColumn = vscode.ViewColumn.Active + + // Check if we should open in the same group as the original file + const provider = this.providerRef?.deref() + if (provider) { + const state = await provider.getState() + const openTabsInCorrectGroup = state?.openTabsInCorrectGroup ?? false + + if (openTabsInCorrectGroup) { + // Find which tab group contains the original file + const originalFileTab = vscode.window.tabGroups.all + .flatMap((group) => group.tabs.map((tab) => ({ tab, group }))) + .find( + ({ tab }) => + tab.input instanceof vscode.TabInputText && arePathsEqual(tab.input.uri.fsPath, uri.fsPath), + ) + + if (originalFileTab) { + // Use the view column of the group containing the original file + targetViewColumn = originalFileTab.group.viewColumn + } else { + // If the original file isn't open, try to find the most logical group + // This could be the group with the most related files (same directory) + const fileDir = path.dirname(uri.fsPath) + const groupsWithRelatedFiles = vscode.window.tabGroups.all.map((group) => { + const relatedFilesCount = group.tabs.filter((tab) => { + if (tab.input instanceof vscode.TabInputText) { + const tabDir = path.dirname(tab.input.uri.fsPath) + return tabDir === fileDir + } + return false + }).length + return { group, relatedFilesCount } + }) + + // Sort by most related files + groupsWithRelatedFiles.sort((a, b) => b.relatedFilesCount - a.relatedFilesCount) + + // Use the group with the most related files, or fall back to active + if (groupsWithRelatedFiles.length > 0 && groupsWithRelatedFiles[0].relatedFilesCount > 0) { + targetViewColumn = groupsWithRelatedFiles[0].group.viewColumn + } + } + } + } + // Open new diff editor. return new Promise((resolve, reject) => { const fileName = path.basename(uri.fsPath) @@ -523,7 +582,7 @@ export class DiffViewProvider { // Pre-open the file as a text document to ensure it doesn't open in preview mode // This fixes issues with files that have custom editor associations (like markdown preview) vscode.window - .showTextDocument(uri, { preview: false, viewColumn: vscode.ViewColumn.Active, preserveFocus: true }) + .showTextDocument(uri, { preview: false, viewColumn: targetViewColumn, preserveFocus: true }) .then(() => { // Execute the diff command after ensuring the file is open as text return vscode.commands.executeCommand( @@ -533,7 +592,7 @@ export class DiffViewProvider { }), uri, `${fileName}: ${fileExists ? `${DIFF_VIEW_LABEL_CHANGES}` : "New File"} (Editable)`, - { preserveFocus: true }, + { preserveFocus: true, viewColumn: targetViewColumn }, ) }) .then( diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 4f2aa2da159..61f1c72d0c0 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -217,6 +217,7 @@ export type ExtensionState = Pick< | "terminalCompressProgressBar" | "diagnosticsEnabled" | "diffEnabled" + | "openTabsInCorrectGroup" | "fuzzyMatchThreshold" // | "experiments" // Optional in GlobalSettings, required here. | "language" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 1f56829f7b3..706ec52ba20 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -93,6 +93,7 @@ export interface WebviewMessage { | "ttsSpeed" | "soundVolume" | "diffEnabled" + | "openTabsInCorrectGroup" | "enableCheckpoints" | "browserViewportSize" | "screenshotQuality" diff --git a/webview-ui/src/components/settings/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx index e1d3c52cb93..a7954a11f74 100644 --- a/webview-ui/src/components/settings/AutoApproveSettings.tsx +++ b/webview-ui/src/components/settings/AutoApproveSettings.tsx @@ -33,6 +33,7 @@ type AutoApproveSettingsProps = HTMLAttributes & { followupAutoApproveTimeoutMs?: number allowedCommands?: string[] deniedCommands?: string[] + openTabsInCorrectGroup?: boolean setCachedStateField: SetCachedStateField< | "alwaysAllowReadOnly" | "alwaysAllowReadOnlyOutsideWorkspace" @@ -52,6 +53,7 @@ type AutoApproveSettingsProps = HTMLAttributes & { | "allowedCommands" | "deniedCommands" | "alwaysAllowUpdateTodoList" + | "openTabsInCorrectGroup" > } @@ -74,6 +76,7 @@ export const AutoApproveSettings = ({ alwaysAllowUpdateTodoList, allowedCommands, deniedCommands, + openTabsInCorrectGroup, setCachedStateField, ...props }: AutoApproveSettingsProps) => { @@ -393,6 +396,27 @@ export const AutoApproveSettings = ({ )} + + {/* FILE EDITING OPTIONS */} +
+
+ +
{t("settings:autoApprove.fileEditing.label")}
+
+
+ setCachedStateField("openTabsInCorrectGroup", e.target.checked)} + data-testid="open-tabs-in-correct-group-checkbox"> + + {t("settings:autoApprove.fileEditing.openTabsInCorrectGroup.label")} + + +
+ {t("settings:autoApprove.fileEditing.openTabsInCorrectGroup.description")} +
+
+
) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 517c1c159d8..e661d318a9d 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -177,6 +177,7 @@ const SettingsView = forwardRef(({ onDone, t alwaysAllowFollowupQuestions, alwaysAllowUpdateTodoList, followupAutoApproveTimeoutMs, + openTabsInCorrectGroup, } = cachedState const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration]) @@ -300,6 +301,7 @@ const SettingsView = forwardRef(({ 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: "openTabsInCorrectGroup", bool: openTabsInCorrectGroup }) vscode.postMessage({ type: "screenshotQuality", value: screenshotQuality ?? 75 }) vscode.postMessage({ type: "terminalOutputLineLimit", value: terminalOutputLineLimit ?? 500 }) vscode.postMessage({ type: "terminalOutputCharacterLimit", value: terminalOutputCharacterLimit ?? 50000 }) @@ -619,6 +621,7 @@ const SettingsView = forwardRef(({ onDone, t followupAutoApproveTimeoutMs={followupAutoApproveTimeoutMs} allowedCommands={allowedCommands} deniedCommands={deniedCommands} + openTabsInCorrectGroup={openTabsInCorrectGroup} setCachedStateField={setCachedStateField} /> )} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index c970733fbad..8ecf68313d3 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -134,6 +134,8 @@ export interface ExtensionStateContextType extends ExtensionState { routerModels?: RouterModels alwaysAllowUpdateTodoList?: boolean setAlwaysAllowUpdateTodoList: (value: boolean) => void + openTabsInCorrectGroup?: boolean + setOpenTabsInCorrectGroup: (value: boolean) => void } export const ExtensionStateContext = createContext(undefined) @@ -229,6 +231,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }, codebaseIndexModels: { ollama: {}, openai: {} }, alwaysAllowUpdateTodoList: true, + openTabsInCorrectGroup: false, }) const [didHydrateState, setDidHydrateState] = useState(false) @@ -474,6 +477,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setAlwaysAllowUpdateTodoList: (value) => { setState((prevState) => ({ ...prevState, alwaysAllowUpdateTodoList: value })) }, + openTabsInCorrectGroup: state.openTabsInCorrectGroup, + setOpenTabsInCorrectGroup: (value) => { + setState((prevState) => ({ ...prevState, openTabsInCorrectGroup: value })) + }, } return {children} diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 7e3c2e3fccb..d5a10ba1f65 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -197,7 +197,14 @@ }, "toggleAriaLabel": "Toggle auto-approval", "disabledAriaLabel": "Auto-approval disabled - select options first", - "selectOptionsFirst": "Select at least one option below to enable auto-approval" + "selectOptionsFirst": "Select at least one option below to enable auto-approval", + "fileEditing": { + "label": "File Editing Options", + "openTabsInCorrectGroup": { + "label": "Open tabs in correct group", + "description": "When enabled, Roo will open diff tabs beside the original file in the same editor group. When disabled, tabs open in the currently active group." + } + } }, "providers": { "providerDocumentation": "{{provider}} documentation",