diff --git a/src/core/Cline.ts b/src/core/Cline.ts index c1c5bf186a..bbaa03d62a 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -1272,10 +1272,16 @@ export class Cline { // This is the most reliable way to know when we're close to hitting the context window. if (totalTokens >= maxAllowedSize) { + // Since the user may switch between models with different context windows, truncating half may not be enough (ie if switching from claude 200k to deepseek 64k, half truncation will only remove 100k tokens, but we need to remove much more) + // So if totalTokens/2 is greater than maxAllowedSize, we truncate 3/4 instead of 1/2 + // FIXME: truncating the conversation in a way that is optimal for prompt caching AND takes into account multi-context window complexity is something we need to improve + const keep = totalTokens / 2 > maxAllowedSize ? "quarter" : "half" + // NOTE: it's okay that we overwriteConversationHistory in resume task since we're only ever removing the last user message and not anything in the middle which would affect this range this.conversationHistoryDeletedRange = getNextTruncationRange( this.apiConversationHistory, this.conversationHistoryDeletedRange, + keep, ) await this.saveClineMessages() // saves task history item which we use to keep track of conversation history deleted range // await this.overwriteApiConversationHistory(truncatedMessages) diff --git a/src/core/sliding-window/index.ts b/src/core/sliding-window/index.ts index 83b91eb381..45d4875bfd 100644 --- a/src/core/sliding-window/index.ts +++ b/src/core/sliding-window/index.ts @@ -55,13 +55,21 @@ truncated = getTruncatedMessages(messages, deletedRange); export function getNextTruncationRange( messages: Anthropic.Messages.MessageParam[], currentDeletedRange: [number, number] | undefined = undefined, + keep: "half" | "quarter" = "half", ): [number, number] { // Since we always keep the first message, currentDeletedRange[0] will always be 1 (for now until we have a smarter truncation algorithm) const rangeStartIndex = 1 const startOfRest = currentDeletedRange ? currentDeletedRange[1] + 1 : 1 - // Remove half of user-assistant pairs - const messagesToRemove = Math.floor((messages.length - startOfRest) / 4) * 2 // Keep even number + let messagesToRemove: number + if (keep === "half") { + // Remove half of user-assistant pairs + messagesToRemove = Math.floor((messages.length - startOfRest) / 4) * 2 // Keep even number + } else { + // Remove 3/4 of user-assistant pairs + messagesToRemove = Math.floor((messages.length - startOfRest) / 8) * 3 * 2 + } + let rangeEndIndex = startOfRest + messagesToRemove - 1 // Make sure the last message being removed is a user message, so that the next message after the initial task message is an assistant message. This preservers the user-assistant-user-assistant structure. diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 3f1acb9a2d..559d2220d5 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -74,6 +74,9 @@ type GlobalStateKey = | "vsCodeLmModelSelector" | "localeLanguage" | "userInfo" + | "previousModeApiProvider" + | "previousModeModelId" + | "previousModeModelInfo" export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -501,6 +504,71 @@ export class ClineProvider implements vscode.WebviewViewProvider { case "chatSettings": if (message.chatSettings) { const didSwitchToActMode = message.chatSettings.mode === "act" + + // Get previous model info that we will revert to after saving current mode api info + const { + apiConfiguration, + previousModeApiProvider: newApiProvider, + previousModeModelId: newModelId, + previousModeModelInfo: newModelInfo, + } = await this.getState() + + // Save the last model used in this mode + await this.updateGlobalState("previousModeApiProvider", apiConfiguration.apiProvider) + switch (apiConfiguration.apiProvider) { + case "anthropic": + case "bedrock": + case "vertex": + case "gemini": + await this.updateGlobalState("previousModeModelId", apiConfiguration.apiModelId) + break + case "openrouter": + await this.updateGlobalState("previousModeModelId", apiConfiguration.openRouterModelId) + await this.updateGlobalState("previousModeModelInfo", apiConfiguration.openRouterModelInfo) + break + case "vscode-lm": + await this.updateGlobalState("previousModeModelId", apiConfiguration.vsCodeLmModelSelector) + break + case "openai": + await this.updateGlobalState("previousModeModelId", apiConfiguration.openAiModelId) + break + case "ollama": + await this.updateGlobalState("previousModeModelId", apiConfiguration.ollamaModelId) + break + case "lmstudio": + await this.updateGlobalState("previousModeModelId", apiConfiguration.lmStudioModelId) + break + } + + // Restore the model used in previous mode + if (newApiProvider && newModelId) { + await this.updateGlobalState("apiProvider", newApiProvider) + switch (newApiProvider) { + case "anthropic": + case "bedrock": + case "vertex": + case "gemini": + await this.updateGlobalState("apiModelId", newModelId) + break + case "openrouter": + await this.updateGlobalState("openRouterModelId", newModelId) + await this.updateGlobalState("openRouterModelInfo", newModelInfo) + break + case "vscode-lm": + await this.updateGlobalState("vsCodeLmModelSelector", newModelId) + break + case "openai": + await this.updateGlobalState("openAiModelId", newModelId) + break + case "ollama": + await this.updateGlobalState("ollamaModelId", newModelId) + break + case "lmstudio": + await this.updateGlobalState("lmStudioModelId", newModelId) + break + } + } + await this.updateGlobalState("chatSettings", message.chatSettings) await this.postStateToWebview() if (this.cline) { @@ -1198,10 +1266,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { browserSettings, chatSettings, userInfo, + authToken, localeLanguage, } = await this.getState() - const authToken = await this.getSecret("authToken") return { version: this.context.extension?.packageJSON?.version ?? "", apiConfiguration, @@ -1310,6 +1378,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { vsCodeLmModelSelector, localeLanguage, userInfo, + authToken, + previousModeApiProvider, + previousModeModelId, + previousModeModelInfo, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -1346,6 +1418,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("vsCodeLmModelSelector") as Promise, this.getGlobalState("localeLanguage") as Promise, this.getGlobalState("userInfo") as Promise, + this.getSecret("authToken") as Promise, + this.getGlobalState("previousModeApiProvider") as Promise, + this.getGlobalState("previousModeModelId") as Promise, + this.getGlobalState("previousModeModelInfo") as Promise, ]) let apiProvider: ApiProvider @@ -1400,6 +1476,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { chatSettings: chatSettings || DEFAULT_CHAT_SETTINGS, localeLanguage, userInfo, + authToken, + previousModeApiProvider, + previousModeModelId, + previousModeModelInfo, } } diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index a7ff649928..ad125600b7 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -586,20 +586,40 @@ const ChatTextArea = forwardRef( [updateCursorPosition], ) + // Separate the API config submission logic + const submitApiConfig = useCallback(() => { + const apiValidationResult = validateApiConfiguration(apiConfiguration) + const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels) + + if (!apiValidationResult && !modelIdValidationResult) { + vscode.postMessage({ type: "apiConfiguration", apiConfiguration }) + } else { + vscode.postMessage({ type: "getLatestState" }) + } + }, [apiConfiguration, openRouterModels]) + const onModeToggle = useCallback(() => { if (textAreaDisabled) return - const newMode = chatSettings.mode === "plan" ? "act" : "plan" - vscode.postMessage({ - type: "chatSettings", - chatSettings: { - mode: newMode, - }, - }) - // Focus the textarea after mode toggle with slight delay + let changeModeDelay = 0 + if (showModelSelector) { + // user has model selector open, so we should save it before switching modes + submitApiConfig() + changeModeDelay = 250 // necessary to let the api config update (we send message and wait for it to be saved) FIXME: this is a hack and we ideally should check for api config changes, then wait for it to be saved, before switching modes + } setTimeout(() => { - textAreaRef.current?.focus() - }, 100) - }, [chatSettings.mode, textAreaDisabled]) + const newMode = chatSettings.mode === "plan" ? "act" : "plan" + vscode.postMessage({ + type: "chatSettings", + chatSettings: { + mode: newMode, + }, + }) + // Focus the textarea after mode toggle with slight delay + setTimeout(() => { + textAreaRef.current?.focus() + }, 100) + }, changeModeDelay) + }, [chatSettings.mode, textAreaDisabled, showModelSelector, submitApiConfig]) const handleContextButtonClick = useCallback(() => { if (textAreaDisabled) return @@ -644,18 +664,6 @@ const ChatTextArea = forwardRef( updateHighlights() }, [inputValue, textAreaDisabled, handleInputChange, updateHighlights]) - // Separate the API config submission logic - const submitApiConfig = useCallback(() => { - const apiValidationResult = validateApiConfiguration(apiConfiguration) - const modelIdValidationResult = validateModelId(apiConfiguration, openRouterModels) - - if (!apiValidationResult && !modelIdValidationResult) { - vscode.postMessage({ type: "apiConfiguration", apiConfiguration }) - } else { - vscode.postMessage({ type: "getLatestState" }) - } - }, [apiConfiguration, openRouterModels]) - // Use an effect to detect menu close useEffect(() => { if (prevShowModelSelector.current && !showModelSelector) {