fix: implement tab-specific mode/model state management

- Add tab-specific state properties to ClineProvider (tabSpecificMode, tabSpecificApiConfigName)
- Create activateProviderProfileForTab method for tab-specific profile activation
- Update handleModeSwitch to use tab-specific mode instead of global state
- Modify getState methods to prioritize tab-specific state over global state
- Update webview message handlers to use tab-specific methods when available

This ensures each Roo tab maintains its own independent mode and model selection,
fixing the issue where changing mode/model in one tab affected all other tabs.

Fixes #8044
This commit is contained in:
Roo Code 2025-09-16 23:22:22 +00:00
parent 2263d86a20
commit bb529ead01
2 changed files with 58 additions and 9 deletions

View file

@ -139,6 +139,11 @@ export class ClineProvider
private pendingOperations: Map<string, PendingEditOperation> = new Map()
private static readonly PENDING_OPERATION_TIMEOUT_MS = 30000 // 30 seconds
// Tab-specific state for mode and model
private tabSpecificMode?: Mode
private tabSpecificApiConfigName?: string
private readonly instanceId: string = Math.random().toString(36).substring(7)
public isViewLaunched = false
public settingsImportedAt?: number
public readonly latestAnnouncementId = "sep-2025-roo-code-cloud" // Roo Code Cloud announcement
@ -1161,7 +1166,8 @@ export class ClineProvider
}
}
await this.updateGlobalState("mode", newMode)
// Update tab-specific mode instead of global state
this.tabSpecificMode = newMode
this.emit(RooCodeEventName.ModeChanged, newMode)
@ -1177,11 +1183,11 @@ export class ClineProvider
const profile = listApiConfig.find(({ id }) => id === savedConfigId)
if (profile?.name) {
await this.activateProviderProfile({ name: profile.name })
await this.activateProviderProfileForTab({ name: profile.name })
}
} else {
// If no saved config for this mode, save current config as default.
const currentApiConfigName = this.getGlobalState("currentApiConfigName")
const currentApiConfigName = this.tabSpecificApiConfigName || this.getGlobalState("currentApiConfigName")
if (currentApiConfigName) {
const config = listApiConfig.find((c) => c.name === currentApiConfigName)
@ -1318,6 +1324,39 @@ export class ClineProvider
}
}
// New method for tab-specific provider profile activation
async activateProviderProfileForTab(args: { name: string } | { id: string }) {
const { name, id, ...providerSettings } = await this.providerSettingsManager.activateProfile(args)
// Update tab-specific API config name
this.tabSpecificApiConfigName = name
// Update list metadata globally (this is fine to share)
await this.contextProxy.setValue("listApiConfigMeta", await this.providerSettingsManager.listConfig())
// Store provider settings in context for this tab's use
// Note: We don't update global currentApiConfigName to avoid affecting other tabs
const mode = this.tabSpecificMode || this.getGlobalState("mode") || defaultModeSlug
if (id) {
await this.providerSettingsManager.setModeConfig(mode, id)
}
// Change the provider for the current task.
const task = this.getCurrentTask()
if (task) {
task.api = buildApiHandler(providerSettings)
}
await this.postStateToWebview()
if (providerSettings.apiProvider) {
this.emit(RooCodeEventName.ProviderProfileChanged, { name, provider: providerSettings.apiProvider })
}
}
async updateCustomInstructions(instructions?: string) {
// User may be clearing the field.
await this.updateGlobalState("customInstructions", instructions || undefined)
@ -1855,10 +1894,10 @@ export class ClineProvider
enableMcpServerCreation: enableMcpServerCreation ?? true,
alwaysApproveResubmit: alwaysApproveResubmit ?? false,
requestDelaySeconds: requestDelaySeconds ?? 10,
currentApiConfigName: currentApiConfigName ?? "default",
currentApiConfigName: this.tabSpecificApiConfigName ?? currentApiConfigName ?? "default",
listApiConfigMeta: listApiConfigMeta ?? [],
pinnedApiConfigs: pinnedApiConfigs ?? {},
mode: mode ?? defaultModeSlug,
mode: this.tabSpecificMode ?? mode ?? defaultModeSlug,
customModePrompts: customModePrompts ?? {},
customSupportPrompts: customSupportPrompts ?? {},
enhancementApiConfigId,
@ -2070,13 +2109,13 @@ export class ClineProvider
terminalZshP10k: stateValues.terminalZshP10k ?? false,
terminalZdotdir: stateValues.terminalZdotdir ?? false,
terminalCompressProgressBar: stateValues.terminalCompressProgressBar ?? true,
mode: stateValues.mode ?? defaultModeSlug,
mode: this.tabSpecificMode ?? stateValues.mode ?? defaultModeSlug,
language: stateValues.language ?? formatLanguage(vscode.env.language),
mcpEnabled: stateValues.mcpEnabled ?? true,
enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true,
alwaysApproveResubmit: stateValues.alwaysApproveResubmit ?? false,
requestDelaySeconds: Math.max(5, stateValues.requestDelaySeconds ?? 10),
currentApiConfigName: stateValues.currentApiConfigName ?? "default",
currentApiConfigName: this.tabSpecificApiConfigName ?? stateValues.currentApiConfigName ?? "default",
listApiConfigMeta: stateValues.listApiConfigMeta ?? [],
pinnedApiConfigs: stateValues.pinnedApiConfigs ?? {},
modeApiConfigs: stateValues.modeApiConfigs ?? ({} as Record<Mode, string>),

View file

@ -1848,7 +1848,12 @@ export const webviewMessageHandler = async (
case "loadApiConfiguration":
if (message.text) {
try {
await provider.activateProviderProfile({ name: message.text })
// Use tab-specific activation method if available
if (typeof provider.activateProviderProfileForTab === "function") {
await provider.activateProviderProfileForTab({ name: message.text })
} else {
await provider.activateProviderProfile({ name: message.text })
}
} catch (error) {
provider.log(
`Error load api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
@ -1860,7 +1865,12 @@ export const webviewMessageHandler = async (
case "loadApiConfigurationById":
if (message.text) {
try {
await provider.activateProviderProfile({ id: message.text })
// Use tab-specific activation method if available
if (typeof provider.activateProviderProfileForTab === "function") {
await provider.activateProviderProfileForTab({ id: message.text })
} else {
await provider.activateProviderProfile({ id: message.text })
}
} catch (error) {
provider.log(
`Error load api configuration by ID: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,