Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions apps/vscode-e2e/src/suite/subtasks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,8 @@ suite("Roo Code Subtasks", function () {
...parentProfile,
openRouterModelId: "openai/gpt-4.1-mini",
}
const priorModeApiConfigs = api.getConfiguration().modeApiConfigs ?? {}
const priorConfiguration = api.getConfiguration()
const priorActiveProfile = api.getActiveProfile()
const parentProfileId = await api.upsertProfile("subtask-parent-profile", parentProfile, true)
const childProfileId = await api.upsertProfile("subtask-child-profile", childProfile, false)
await api.setConfiguration({
Expand Down Expand Up @@ -735,7 +736,10 @@ suite("Roo Code Subtasks", function () {
)
} finally {
api.off(RooCodeEventName.Message, messageHandler)
await api.setConfiguration({ modeApiConfigs: priorModeApiConfigs })
await api.setConfiguration(priorConfiguration)
if (priorActiveProfile) {
await api.setActiveProfile(priorActiveProfile)
}
await api.deleteProfile("subtask-child-profile").catch(() => {})
await api.deleteProfile("subtask-parent-profile").catch(() => {})
while (api.getCurrentTaskStack().length > 0) {
Expand Down
129 changes: 110 additions & 19 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,62 @@ export class ClineProvider
private globalStateWriteThroughTimer: ReturnType<typeof setTimeout> | null = null
private static readonly GLOBAL_STATE_WRITE_THROUGH_DEBOUNCE_MS = 5000 // 5 seconds
private static readonly PENDING_OPERATION_TIMEOUT_MS = 30000 // 30 seconds
private providerProfileMutationQueue = Promise.resolve()

private runDelegationTransition<T>(parentTaskId: string, fn: () => Promise<T>): Promise<T> {
this.delegationTransitionLocks ??= new Map()
return runDelegationTransition(this.delegationTransitionLocks, parentTaskId, fn)
}

private enqueueProviderProfileMutation<T>(fn: () => Promise<T>): Promise<T> {
const run = this.providerProfileMutationQueue.then(fn, fn)
let timedOut = false
const callerResult = this.withProviderProfileMutationTimeout(run, () => {
timedOut = true
this.log("Provider profile mutation timed out; releasing the mutation queue")
})

void run.then(
() => {
if (timedOut) {
this.log("Provider profile mutation completed after timing out")
}
},
(error) => {
if (timedOut) {
this.log(
`Provider profile mutation failed after timing out: ${
error instanceof Error ? error.message : String(error)
}`,
)
}
},
)

// Advance from the timeout-bounded caller result, rather than the raw operation. A
// provider call that never settles must not block all subsequent profile changes.
this.providerProfileMutationQueue = callerResult.then(
() => undefined,
() => undefined,
)
return callerResult
}

private withProviderProfileMutationTimeout<T>(operation: Promise<T>, onTimeout: () => void): Promise<T> {
let timeoutId: ReturnType<typeof setTimeout> | undefined
const timeout = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => {
onTimeout()
reject(new Error("Provider profile mutation timed out"))
}, ClineProvider.PENDING_OPERATION_TIMEOUT_MS)
})

return Promise.race([operation, timeout]).finally(() => {
if (timeoutId) {
clearTimeout(timeoutId)
}
})
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
private readonly pendingEditOperations: PendingEditOperationStore

private cloudOrganizationsCache: CloudOrganizationMembership[] | null = null
Expand Down Expand Up @@ -1506,9 +1557,17 @@ export class ClineProvider
/**
* Handle switching to a new mode, including updating the associated API configuration
* @param newMode The mode to switch to
* @param targetTask The task whose in-memory mode should be updated. Defaults to the
* current task. Pass null to apply only global mode/profile effects for a pending child.
*/
public async handleModeSwitch(newMode: Mode) {
const task = this.getCurrentTask()
public async handleModeSwitch(newMode: Mode, targetTask?: Task | null) {
return this.enqueueProviderProfileMutation(() =>
this.handleModeSwitchUnlocked(newMode, targetTask === undefined ? this.getCurrentTask() : targetTask),
)
}

private async handleModeSwitchUnlocked(newMode: Mode, targetTask: Task | null | undefined): Promise<void> {
const task = targetTask
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if (task) {
TelemetryService.instance.captureModeSwitch(task.taskId, newMode)
Expand Down Expand Up @@ -1545,7 +1604,9 @@ export class ClineProvider
// If workspace lock is on, keep the current API config — don't load mode-specific config
const lockApiConfigAcrossModes = this.context.workspaceState.get("lockApiConfigAcrossModes", false)
if (lockApiConfigAcrossModes) {
await this.postStateToWebview()
if (targetTask !== null) {
await this.postStateToWebview()
}
return
}

Expand All @@ -1571,7 +1632,10 @@ export class ClineProvider
const hasActualSettings = !!fullProfile.apiProvider

if (hasActualSettings) {
await this.activateProviderProfile({ name: profile.name })
await this.activateProviderProfileUnlocked(
{ name: profile.name },
targetTask === null ? { skipCurrentTaskRebuild: true } : undefined,
)
} else {
// The task will continue with the current/default configuration.
}
Expand All @@ -1591,7 +1655,9 @@ export class ClineProvider
}
}

await this.postStateToWebview()
if (targetTask !== null) {
await this.postStateToWebview()
}
}

// Provider Profile Management
Expand All @@ -1607,8 +1673,9 @@ export class ClineProvider
*/
private updateTaskApiHandlerIfNeeded(
providerSettings: ProviderSettings,
options: { forceRebuild?: boolean } = {},
options: { forceRebuild?: boolean; skipCurrentTaskRebuild?: boolean } = {},
): void {
if (options.skipCurrentTaskRebuild) return
const task = this.getCurrentTask()
if (!task) return

Expand Down Expand Up @@ -1724,7 +1791,11 @@ export class ClineProvider
await this.postStateToWebview()
}

private async persistStickyProviderProfileToCurrentTask(apiConfigName: string): Promise<void> {
private async persistStickyProviderProfileToCurrentTask(
apiConfigName: string,
options: { skipCurrentTaskRebuild?: boolean } = {},
): Promise<void> {
if (options.skipCurrentTaskRebuild) return
const task = this.getCurrentTask()
if (!task) {
return
Expand Down Expand Up @@ -1754,19 +1825,37 @@ export class ClineProvider

async activateProviderProfile(
args: { name: string } | { id: string },
options?: { persistModeConfig?: boolean; persistTaskHistory?: boolean },
options?: {
persistModeConfig?: boolean
persistTaskHistory?: boolean
skipCurrentTaskRebuild?: boolean
},
) {
return this.enqueueProviderProfileMutation(() => this.activateProviderProfileUnlocked(args, options))
}

private async activateProviderProfileUnlocked(
args: { name: string } | { id: string },
options?: {
persistModeConfig?: boolean
persistTaskHistory?: boolean
skipCurrentTaskRebuild?: boolean
},
): Promise<void> {
const { name, id, ...providerSettings } = await this.providerSettingsManager.activateProfile(args)

const persistModeConfig = options?.persistModeConfig ?? true
const persistTaskHistory = options?.persistTaskHistory ?? true

// See `upsertProviderProfile` for a description of what this is doing.
await Promise.all([
this.contextProxy.setValue("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
this.contextProxy.setValue("currentApiConfigName", name),
this.contextProxy.setProviderSettings(providerSettings),
])
const skipCurrentTaskRebuild = options?.skipCurrentTaskRebuild ?? false

if (!skipCurrentTaskRebuild) {
// See `upsertProviderProfile` for a description of what this is doing.
await Promise.all([
this.contextProxy.setValue("listApiConfigMeta", await this.providerSettingsManager.listConfig()),
this.contextProxy.setValue("currentApiConfigName", name),
this.contextProxy.setProviderSettings(providerSettings),
])
}

const { mode } = await this.getState()

Expand All @@ -1775,17 +1864,19 @@ export class ClineProvider
}

// Change the provider for the current task.
this.updateTaskApiHandlerIfNeeded(providerSettings, { forceRebuild: true })
this.updateTaskApiHandlerIfNeeded(providerSettings, { forceRebuild: true, skipCurrentTaskRebuild })

// Update the current task's sticky provider profile, unless this activation is
// being used purely as a non-persisting restoration (e.g., reopening a task from history).
if (persistTaskHistory) {
await this.persistStickyProviderProfileToCurrentTask(name)
await this.persistStickyProviderProfileToCurrentTask(name, { skipCurrentTaskRebuild })
}

await this.postStateToWebview()
if (!skipCurrentTaskRebuild) {
await this.postStateToWebview()
}

if (providerSettings.apiProvider) {
if (providerSettings.apiProvider && !skipCurrentTaskRebuild) {
this.emit(RooCodeEventName.ProviderProfileChanged, { name, provider: providerSettings.apiProvider })
}
}
Expand Down
Loading
Loading