From e509e1563687bdbc39b7bfba881b3deef58917e0 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 25 Mar 2026 14:36:09 +0000 Subject: [PATCH] feat: restore dedicated model selector for context condensing Adds a condensingApiConfigId setting that allows users to select a dedicated API configuration for context condensing operations. This restores the ability to use a cheaper/faster model (e.g. Gemini Flash) for condensing while keeping an expensive model (e.g. Claude Opus) as the active coding model. Changes: - Add condensingApiConfigId to global settings schema and ExtensionState - Add webview message handler for condensingApiConfigId - Build dedicated API handler in Task.ts for manual and auto condensing - Add API config dropdown in ContextManagementSettings UI - Update tests for new combobox count and default props Closes #11785 --- packages/types/src/global-settings.ts | 1 + packages/types/src/vscode-extension-host.ts | 2 + src/core/task/Task.ts | 51 +++++++++++++++++-- src/core/webview/ClineProvider.ts | 3 ++ src/core/webview/webviewMessageHandler.ts | 4 ++ .../settings/ContextManagementSettings.tsx | 50 ++++++++++++++++++ .../src/components/settings/SettingsView.tsx | 4 ++ .../ContextManagementSettings.spec.tsx | 10 ++-- 8 files changed, 118 insertions(+), 7 deletions(-) diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 288f6c2118c..0a28e5fa081 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -198,6 +198,7 @@ export const globalSettingsSchema = z.object({ customModePrompts: customModePromptsSchema.optional(), customSupportPrompts: customSupportPromptsSchema.optional(), enhancementApiConfigId: z.string().optional(), + condensingApiConfigId: z.string().optional(), includeTaskHistoryInEnhance: z.boolean().optional(), historyPreviewCollapsed: z.boolean().optional(), reasoningBlockCollapsed: z.boolean().optional(), diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index b20539afe49..fa64cca6c57 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -289,6 +289,7 @@ export type ExtensionState = Pick< | "customModePrompts" | "customSupportPrompts" | "enhancementApiConfigId" + | "condensingApiConfigId" | "customCondensingPrompt" | "codebaseIndexConfig" | "codebaseIndexModels" @@ -485,6 +486,7 @@ export interface WebviewMessage { | "copySystemPrompt" | "systemPrompt" | "enhancementApiConfigId" + | "condensingApiConfigId" | "autoApprovalEnabled" | "updateCustomMode" | "deleteCustomMode" diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 005bb0f292b..e3aeafaa712 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1645,6 +1645,42 @@ export class Task extends EventEmitter implements TaskLike { } } + /** + * Builds a dedicated API handler for context condensing if a condensing API config is set. + * Falls back to the task's main API handler if no dedicated config is configured. + */ + private async buildCondensingApiHandler(): Promise { + const provider = this.providerRef.deref() + if (!provider) { + return this.api + } + + const state = await provider.getState() + const { condensingApiConfigId, listApiConfigMeta } = state ?? {} + + if ( + condensingApiConfigId && + listApiConfigMeta?.find(({ id }: { id: string }) => id === condensingApiConfigId) + ) { + try { + const { name: _, ...providerSettings } = await provider.providerSettingsManager.getProfile({ + id: condensingApiConfigId, + }) + + if (providerSettings.apiProvider) { + return buildApiHandler(providerSettings) + } + } catch (error) { + console.warn( + `[Task#${this.taskId}] Failed to build condensing API handler, falling back to task handler:`, + error, + ) + } + } + + return this.api + } + public async condenseContext(): Promise { // CRITICAL: Flush any pending tool results before condensing // to ensure tool_use/tool_result pairs are complete in history @@ -1659,6 +1695,9 @@ export class Task extends EventEmitter implements TaskLike { const { contextTokens: prevContextTokens } = this.getTokenUsage() + // Build a dedicated API handler for condensing if configured + const condensingApiHandler = await this.buildCondensingApiHandler() + // Build tools for condensing metadata (same tools used for normal API calls) const provider = this.providerRef.deref() let allTools: import("openai").default.Chat.ChatCompletionTool[] = [] @@ -1705,7 +1744,7 @@ export class Task extends EventEmitter implements TaskLike { condenseId, } = await summarizeConversation({ messages: this.apiConversationHistory, - apiHandler: this.api, + apiHandler: condensingApiHandler, systemPrompt, taskId: this.taskId, isAutomaticTrigger: false, @@ -3887,13 +3926,16 @@ export class Task extends EventEmitter implements TaskLike { // Generate environment details to include in the condensed summary const environmentDetails = await getEnvironmentDetails(this, true) + // Build a dedicated API handler for condensing if configured + const condensingApiHandler = await this.buildCondensingApiHandler() + // Force aggressive truncation by keeping only 75% of the conversation history const truncateResult = await manageContext({ messages: this.apiConversationHistory, totalTokens: contextTokens || 0, maxTokens, contextWindow, - apiHandler: this.api, + apiHandler: condensingApiHandler, autoCondenseContext: true, autoCondenseContextPercent: FORCED_CONTEXT_REDUCTION_PERCENT, systemPrompt: await this.getSystemPrompt(), @@ -4112,12 +4154,15 @@ export class Task extends EventEmitter implements TaskLike { : undefined try { + // Build a dedicated API handler for condensing if configured + const condensingApiHandler = await this.buildCondensingApiHandler() + const truncateResult = await manageContext({ messages: this.apiConversationHistory, totalTokens: contextTokens, maxTokens, contextWindow, - apiHandler: this.api, + apiHandler: condensingApiHandler, autoCondenseContext, autoCondenseContextPercent, systemPrompt, diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 7bd969e52d0..00468774e33 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2170,6 +2170,7 @@ export class ClineProvider customModePrompts, customSupportPrompts, enhancementApiConfigId, + condensingApiConfigId, autoApprovalEnabled, customModes, experiments, @@ -2290,6 +2291,7 @@ export class ClineProvider customModePrompts: customModePrompts ?? {}, customSupportPrompts: customSupportPrompts ?? {}, enhancementApiConfigId, + condensingApiConfigId, autoApprovalEnabled: autoApprovalEnabled ?? false, customModes, experiments: experiments ?? experimentDefault, @@ -2520,6 +2522,7 @@ export class ClineProvider customModePrompts: stateValues.customModePrompts ?? {}, customSupportPrompts: stateValues.customSupportPrompts ?? {}, enhancementApiConfigId: stateValues.enhancementApiConfigId, + condensingApiConfigId: stateValues.condensingApiConfigId, experiments: stateValues.experiments ?? experimentDefault, autoApprovalEnabled: stateValues.autoApprovalEnabled ?? false, customModes, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index d27fd6bec09..274a2e1cc79 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1653,6 +1653,10 @@ export const webviewMessageHandler = async ( await updateGlobalState("enhancementApiConfigId", message.text) await provider.postStateToWebview() break + case "condensingApiConfigId": + await updateGlobalState("condensingApiConfigId", message.text) + await provider.postStateToWebview() + break case "autoApprovalEnabled": await updateGlobalState("autoApprovalEnabled", message.bool ?? false) diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index 8663ea6e038..64d802d964c 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -29,6 +29,8 @@ type ContextManagementSettingsProps = HTMLAttributes & { autoCondenseContext: boolean autoCondenseContextPercent: number listApiConfigMeta: any[] + condensingApiConfigId?: string + setCondensingApiConfigId: (value: string) => void maxOpenTabsContext: number maxWorkspaceFiles: number showRooIgnoredFiles?: boolean @@ -67,6 +69,8 @@ export const ContextManagementSettings = ({ autoCondenseContext, autoCondenseContextPercent, listApiConfigMeta, + condensingApiConfigId, + setCondensingApiConfigId, maxOpenTabsContext, maxWorkspaceFiles, showRooIgnoredFiles, @@ -472,6 +476,52 @@ export const ContextManagementSettings = ({ /> + {/* Condensing API Configuration */} + +
+ +
+ {t("settings:contextManagement.condensingApiConfiguration.description")} +
+ +
+
+ {/* Auto Condense Context */} (({ onDone, t autoCondenseContext={autoCondenseContext} autoCondenseContextPercent={autoCondenseContextPercent} listApiConfigMeta={listApiConfigMeta ?? []} + condensingApiConfigId={cachedState.condensingApiConfigId} + setCondensingApiConfigId={(value) => + setCachedState((prev) => ({ ...prev, condensingApiConfigId: value })) + } maxOpenTabsContext={maxOpenTabsContext} maxWorkspaceFiles={maxWorkspaceFiles ?? 200} showRooIgnoredFiles={showRooIgnoredFiles} diff --git a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx index 2de2954c2b9..182f49d4042 100644 --- a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.spec.tsx @@ -89,6 +89,8 @@ describe("ContextManagementSettings", () => { autoCondenseContext: false, autoCondenseContextPercent: 80, listApiConfigMeta: [], + condensingApiConfigId: undefined as string | undefined, + setCondensingApiConfigId: vi.fn(), maxOpenTabsContext: 20, maxWorkspaceFiles: 200, showRooIgnoredFiles: false, @@ -333,9 +335,9 @@ describe("ContextManagementSettings", () => { const slider = screen.getByTestId("condense-threshold-slider") expect(slider).toBeInTheDocument() - // Should render the profile select dropdown + // Should render the profile select dropdown and condensing API config dropdown const selects = screen.getAllByRole("combobox") - expect(selects).toHaveLength(1) + expect(selects).toHaveLength(2) }) describe("Auto Condense Context functionality", () => { @@ -368,8 +370,8 @@ describe("ContextManagementSettings", () => { // Threshold settings should be visible expect(screen.getByTestId("condense-threshold-slider")).toBeInTheDocument() - // One combobox for profile selection - expect(screen.getAllByRole("combobox")).toHaveLength(1) + // Two comboboxes: condensing API config + profile selection + expect(screen.getAllByRole("combobox")).toHaveLength(2) }) it("updates auto condense context percent", () => {