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
This commit is contained in:
Roo Code 2025-07-21 15:20:24 +00:00
parent 9fce90be9d
commit 9dd88bbcf9
11 changed files with 119 additions and 10 deletions

View file

@ -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(),

View file

@ -260,7 +260,7 @@ export class Task extends EventEmitter<ClineEvents> {
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

View file

@ -1726,6 +1726,7 @@ export class ClineProvider
codebaseIndexSearchMinScore: stateValues.codebaseIndexConfig?.codebaseIndexSearchMinScore,
},
profileThresholds: stateValues.profileThresholds ?? {},
openTabsInCorrectGroup: stateValues.openTabsInCorrectGroup ?? false,
}
}

View file

@ -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)

View file

@ -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[]][] = []
private providerRef?: WeakRef<ClineProvider>
constructor(private cwd: string) {}
constructor(
private cwd: string,
provider?: ClineProvider,
) {
if (provider) {
this.providerRef = new WeakRef(provider)
}
}
async open(relPath: string): Promise<void> {
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<vscode.TextEditor>((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(

View file

@ -217,6 +217,7 @@ export type ExtensionState = Pick<
| "terminalCompressProgressBar"
| "diagnosticsEnabled"
| "diffEnabled"
| "openTabsInCorrectGroup"
| "fuzzyMatchThreshold"
// | "experiments" // Optional in GlobalSettings, required here.
| "language"

View file

@ -93,6 +93,7 @@ export interface WebviewMessage {
| "ttsSpeed"
| "soundVolume"
| "diffEnabled"
| "openTabsInCorrectGroup"
| "enableCheckpoints"
| "browserViewportSize"
| "screenshotQuality"

View file

@ -33,6 +33,7 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
followupAutoApproveTimeoutMs?: number
allowedCommands?: string[]
deniedCommands?: string[]
openTabsInCorrectGroup?: boolean
setCachedStateField: SetCachedStateField<
| "alwaysAllowReadOnly"
| "alwaysAllowReadOnlyOutsideWorkspace"
@ -52,6 +53,7 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
| "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 = ({
</div>
</div>
)}
{/* FILE EDITING OPTIONS */}
<div className="mt-6">
<div className="flex items-center gap-4 font-bold mb-3">
<span className="codicon codicon-file-code" />
<div>{t("settings:autoApprove.fileEditing.label")}</div>
</div>
<div className="pl-3 border-l-2 border-vscode-button-background">
<VSCodeCheckbox
checked={openTabsInCorrectGroup}
onChange={(e: any) => setCachedStateField("openTabsInCorrectGroup", e.target.checked)}
data-testid="open-tabs-in-correct-group-checkbox">
<span className="font-medium">
{t("settings:autoApprove.fileEditing.openTabsInCorrectGroup.label")}
</span>
</VSCodeCheckbox>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.fileEditing.openTabsInCorrectGroup.description")}
</div>
</div>
</div>
</Section>
</div>
)

View file

@ -177,6 +177,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
alwaysAllowFollowupQuestions,
alwaysAllowUpdateTodoList,
followupAutoApproveTimeoutMs,
openTabsInCorrectGroup,
} = cachedState
const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
@ -300,6 +301,7 @@ 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: "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<SettingsViewRef, SettingsViewProps>(({ onDone, t
followupAutoApproveTimeoutMs={followupAutoApproveTimeoutMs}
allowedCommands={allowedCommands}
deniedCommands={deniedCommands}
openTabsInCorrectGroup={openTabsInCorrectGroup}
setCachedStateField={setCachedStateField}
/>
)}

View file

@ -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<ExtensionStateContextType | undefined>(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 <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>

View file

@ -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",