From 9dd88bbcf93ba3d48538c65e607d49cfd286e29a Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 21 Jul 2025 15:20:24 +0000 Subject: [PATCH] feat: Add openTabsInCorrectGroup setting for better tab management - Added openTabsInCorrectGroup setting to GlobalState type definition - Created UI toggle in AutoApproveSettings component within File Editing Options section - Added translation strings for the new UI elements - Implemented message handler in webviewMessageHandler.ts - Modified DiffViewProvider to accept provider reference and implement tab placement logic - Updated Task.ts to pass provider to DiffViewProvider - Added setting to ExtensionState and WebviewMessage types - Fixed TypeScript errors by adding openTabsInCorrectGroup to ClineProvider getState() This feature allows Roo to open diff editors and related tabs in the most logical editor group when working with multiple VS Code editor groups, improving navigation and organization for users with complex editor layouts. Fixes #6005 --- packages/types/src/global-settings.ts | 1 + src/core/task/Task.ts | 2 +- src/core/webview/ClineProvider.ts | 1 + src/core/webview/webviewMessageHandler.ts | 5 ++ src/integrations/editor/DiffViewProvider.ts | 77 ++++++++++++++++--- src/shared/ExtensionMessage.ts | 1 + src/shared/WebviewMessage.ts | 1 + .../settings/AutoApproveSettings.tsx | 24 ++++++ .../src/components/settings/SettingsView.tsx | 3 + .../src/context/ExtensionStateContext.tsx | 7 ++ webview-ui/src/i18n/locales/en/settings.json | 9 ++- 11 files changed, 120 insertions(+), 11 deletions(-) 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",