diff --git a/src/core/condense/index.ts b/src/core/condense/index.ts index c9c07a60e3..ed6a607438 100644 --- a/src/core/condense/index.ts +++ b/src/core/condense/index.ts @@ -8,6 +8,7 @@ import { ApiHandler } from "../../api" import { ApiMessage } from "../task-persistence/apiMessages" import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning" import { findLast } from "../../shared/array" +import { supportPrompt } from "../../shared/support-prompt" /** * Checks if a message contains tool_result blocks. @@ -154,45 +155,7 @@ export const N_MESSAGES_TO_KEEP = 3 export const MIN_CONDENSE_THRESHOLD = 5 // Minimum percentage of context window to trigger condensing export const MAX_CONDENSE_THRESHOLD = 100 // Maximum percentage of context window to trigger condensing -const SUMMARY_PROMPT = `\ -Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions. -This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing with the conversation and supporting any continuing tasks. - -Your summary should be structured as follows: -Context: The context to continue the conversation with. If applicable based on the current task, this should include: - 1. Previous Conversation: High level details about what was discussed throughout the entire conversation with the user. This should be written to allow someone to be able to follow the general overarching conversation flow. - 2. Current Work: Describe in detail what was being worked on prior to this request to summarize the conversation. Pay special attention to the more recent messages in the conversation. - 3. Key Technical Concepts: List all important technical concepts, technologies, coding conventions, and frameworks discussed, which might be relevant for continuing with this work. - 4. Relevant Files and Code: If applicable, enumerate specific files and code sections examined, modified, or created for the task continuation. Pay special attention to the most recent messages and changes. - 5. Problem Solving: Document problems solved thus far and any ongoing troubleshooting efforts. - 6. Pending Tasks and Next Steps: Outline all pending tasks that you have explicitly been asked to work on, as well as list the next steps you will take for all outstanding work, if applicable. Include code snippets where they add clarity. For any next steps, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no information loss in context between tasks. - -Example summary structure: -1. Previous Conversation: - [Detailed description] -2. Current Work: - [Detailed description] -3. Key Technical Concepts: - - [Concept 1] - - [Concept 2] - - [...] -4. Relevant Files and Code: - - [File Name 1] - - [Summary of why this file is important] - - [Summary of the changes made to this file, if any] - - [Important Code Snippet] - - [File Name 2] - - [Important Code Snippet] - - [...] -5. Problem Solving: - [Detailed description] -6. Pending Tasks and Next Steps: - - [Task 1 details & next steps] - - [Task 2 details & next steps] - - [...] - -Output only the summary of the conversation so far, without any additional commentary or explanation. -` +const SUMMARY_PROMPT = supportPrompt.default.CONDENSE export type SummarizeResponse = { messages: ApiMessage[] // The messages after summarization diff --git a/src/core/config/ContextProxy.ts b/src/core/config/ContextProxy.ts index 64baf546bd..c3b602ea74 100644 --- a/src/core/config/ContextProxy.ts +++ b/src/core/config/ContextProxy.ts @@ -20,6 +20,7 @@ import { import { TelemetryService } from "@roo-code/telemetry" import { logger } from "../../utils/logging" +import { supportPrompt } from "../../shared/support-prompt" type GlobalStateKey = keyof GlobalState type SecretStateKey = keyof SecretState @@ -92,9 +93,51 @@ export class ContextProxy { // Migration: Sanitize invalid/removed API providers await this.migrateInvalidApiProvider() + // Migration: Move legacy customCondensingPrompt to customSupportPrompts + await this.migrateLegacyCondensingPrompt() + this._isInitialized = true } + /** + * Migrates the legacy customCondensingPrompt to the new customSupportPrompts structure + * and removes the legacy field. + * + * Note: Only true customizations are migrated. If the legacy prompt equals the default, + * we skip the migration to avoid pinning users to an old default if the default changes. + */ + private async migrateLegacyCondensingPrompt() { + try { + const legacyPrompt = this.originalContext.globalState.get("customCondensingPrompt") + if (legacyPrompt) { + const currentSupportPrompts = + this.originalContext.globalState.get>("customSupportPrompts") || {} + + // Only migrate if: + // 1. The new location doesn't already have a value + // 2. The legacy prompt is a true customization (not equal to the default) + // This prevents pinning users to an old default if the default prompt changes. + const isCustomized = legacyPrompt.trim() !== supportPrompt.default.CONDENSE.trim() + if (!currentSupportPrompts.CONDENSE && isCustomized) { + logger.info("Migrating customized legacy customCondensingPrompt to customSupportPrompts") + const updatedPrompts = { ...currentSupportPrompts, CONDENSE: legacyPrompt } + await this.originalContext.globalState.update("customSupportPrompts", updatedPrompts) + this.stateCache.customSupportPrompts = updatedPrompts + } else if (!isCustomized) { + logger.info("Skipping migration: legacy customCondensingPrompt equals the default prompt") + } + + // Always remove the legacy field + await this.originalContext.globalState.update("customCondensingPrompt", undefined) + this.stateCache.customCondensingPrompt = undefined + } + } catch (error) { + logger.error( + `Error during customCondensingPrompt migration: ${error instanceof Error ? error.message : String(error)}`, + ) + } + } + /** * Migrates invalid/removed apiProvider values by clearing them from storage. * This handles cases where a user had a provider selected that was later removed diff --git a/src/core/config/__tests__/ContextProxy.spec.ts b/src/core/config/__tests__/ContextProxy.spec.ts index 49e706b181..bfdbd1619f 100644 --- a/src/core/config/__tests__/ContextProxy.spec.ts +++ b/src/core/config/__tests__/ContextProxy.spec.ts @@ -70,13 +70,16 @@ describe("ContextProxy", () => { describe("constructor", () => { it("should initialize state cache with all global state keys", () => { - // +1 for the migration check of old nested settings - expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 1) + // +2 for the migration checks: + // 1. openRouterImageGenerationSettings + // 2. customCondensingPrompt + expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 2) for (const key of GLOBAL_STATE_KEYS) { expect(mockGlobalState.get).toHaveBeenCalledWith(key) } - // Also check for migration call + // Also check for migration calls expect(mockGlobalState.get).toHaveBeenCalledWith("openRouterImageGenerationSettings") + expect(mockGlobalState.get).toHaveBeenCalledWith("customCondensingPrompt") }) it("should initialize secret cache with all secret keys", () => { @@ -99,8 +102,8 @@ describe("ContextProxy", () => { const result = proxy.getGlobalState("apiProvider") expect(result).toBe("deepseek") - // Original context should be called once during updateGlobalState (+1 for migration check) - expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 1) // From initialization + migration check + // Original context should be called once during updateGlobalState (+2 for migration checks) + expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 2) // From initialization + migration checks }) it("should handle default values correctly", async () => { diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index d9457f81d3..a235cf4824 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -1573,7 +1573,7 @@ export class Task extends EventEmitter implements TaskLike { // Get condensing configuration const state = await this.providerRef.deref()?.getState() // These properties may not exist in the state type yet, but are used for condensing configuration - const customCondensingPrompt = state?.customCondensingPrompt + const customCondensingPrompt = state?.customSupportPrompts?.CONDENSE const condensingApiConfigId = state?.condensingApiConfigId const listApiConfigMeta = state?.listApiConfigMeta @@ -3824,7 +3824,7 @@ export class Task extends EventEmitter implements TaskLike { } = state ?? {} // Get condensing configuration for automatic triggers. - const customCondensingPrompt = state?.customCondensingPrompt + const customCondensingPrompt = state?.customSupportPrompts?.CONDENSE const condensingApiConfigId = state?.condensingApiConfigId const listApiConfigMeta = state?.listApiConfigMeta diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 2af791b93e..e3c9510859 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1650,16 +1650,6 @@ export const webviewMessageHandler = async ( await provider.postStateToWebview() break - case "updateCondensingPrompt": - // Store the condensing prompt in customSupportPrompts["CONDENSE"] - // instead of customCondensingPrompt. - const currentSupportPrompts = getGlobalState("customSupportPrompts") ?? {} - const updatedSupportPrompts = { ...currentSupportPrompts, CONDENSE: message.text } - await updateGlobalState("customSupportPrompts", updatedSupportPrompts) - // Also update the old field for backward compatibility during migration. - await updateGlobalState("customCondensingPrompt", message.text) - await provider.postStateToWebview() - break case "autoApprovalEnabled": await updateGlobalState("autoApprovalEnabled", message.bool ?? false) await provider.postStateToWebview() diff --git a/webview-ui/src/components/settings/PromptsSettings.tsx b/webview-ui/src/components/settings/PromptsSettings.tsx index ce27db44e4..e628919e62 100644 --- a/webview-ui/src/components/settings/PromptsSettings.tsx +++ b/webview-ui/src/components/settings/PromptsSettings.tsx @@ -23,8 +23,6 @@ import { SearchableSetting } from "./SearchableSetting" interface PromptsSettingsProps { customSupportPrompts: Record setCustomSupportPrompts: (prompts: Record) => void - customCondensingPrompt?: string - setCustomCondensingPrompt?: (value: string) => void includeTaskHistoryInEnhance?: boolean setIncludeTaskHistoryInEnhance?: (value: boolean) => void } @@ -32,8 +30,6 @@ interface PromptsSettingsProps { const PromptsSettings = ({ customSupportPrompts, setCustomSupportPrompts, - customCondensingPrompt: propsCustomCondensingPrompt, - setCustomCondensingPrompt: propsSetCustomCondensingPrompt, includeTaskHistoryInEnhance: propsIncludeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance: propsSetIncludeTaskHistoryInEnhance, }: PromptsSettingsProps) => { @@ -44,16 +40,10 @@ const PromptsSettings = ({ setEnhancementApiConfigId, condensingApiConfigId, setCondensingApiConfigId, - customCondensingPrompt: contextCustomCondensingPrompt, - setCustomCondensingPrompt: contextSetCustomCondensingPrompt, includeTaskHistoryInEnhance: contextIncludeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance: contextSetIncludeTaskHistoryInEnhance, } = useExtensionState() - // Use props if provided, otherwise fall back to context - const customCondensingPrompt = propsCustomCondensingPrompt ?? contextCustomCondensingPrompt - const setCustomCondensingPrompt = propsSetCustomCondensingPrompt ?? contextSetCustomCondensingPrompt - // Use props if provided, otherwise fall back to context const includeTaskHistoryInEnhance = propsIncludeTaskHistoryInEnhance ?? contextIncludeTaskHistoryInEnhance ?? true const setIncludeTaskHistoryInEnhance = propsSetIncludeTaskHistoryInEnhance ?? contextSetIncludeTaskHistoryInEnhance @@ -82,46 +72,22 @@ const PromptsSettings = ({ // Use nullish coalescing to preserve empty strings const finalValue = value ?? undefined - if (type === "CONDENSE") { - setCustomCondensingPrompt(finalValue ?? supportPrompt.default.CONDENSE) - // Also update the customSupportPrompts to trigger change detection - const updatedPrompts = { ...customSupportPrompts } - if (finalValue === undefined) { - delete updatedPrompts[type] - } else { - updatedPrompts[type] = finalValue - } - setCustomSupportPrompts(updatedPrompts) + const updatedPrompts = { ...customSupportPrompts } + if (finalValue === undefined) { + delete updatedPrompts[type] } else { - const updatedPrompts = { ...customSupportPrompts } - if (finalValue === undefined) { - delete updatedPrompts[type] - } else { - updatedPrompts[type] = finalValue - } - setCustomSupportPrompts(updatedPrompts) + updatedPrompts[type] = finalValue } + setCustomSupportPrompts(updatedPrompts) } const handleSupportReset = (type: SupportPromptType) => { - if (type === "CONDENSE") { - setCustomCondensingPrompt(supportPrompt.default.CONDENSE) - // Also update the customSupportPrompts to trigger change detection - const updatedPrompts = { ...customSupportPrompts } - delete updatedPrompts[type] - setCustomSupportPrompts(updatedPrompts) - } else { - const updatedPrompts = { ...customSupportPrompts } - delete updatedPrompts[type] - setCustomSupportPrompts(updatedPrompts) - } + const updatedPrompts = { ...customSupportPrompts } + delete updatedPrompts[type] + setCustomSupportPrompts(updatedPrompts) } const getSupportPromptValue = (type: SupportPromptType): string => { - if (type === "CONDENSE") { - // Preserve empty string - only fall back to default when value is nullish - return customCondensingPrompt ?? supportPrompt.default.CONDENSE - } return supportPrompt.get(customSupportPrompts, type) } diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 6c9ee47d1c..5acdeb7ddd 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -198,7 +198,6 @@ const SettingsView = forwardRef(({ onDone, t terminalCompressProgressBar, maxConcurrentFileReads, condensingApiConfigId, - customCondensingPrompt, customSupportPrompts, profileThresholds, alwaysAllowFollowupQuestions, @@ -438,7 +437,6 @@ const SettingsView = forwardRef(({ onDone, t // These have more complex logic so they aren't (yet) handled // by the `updateSettings` message. - vscode.postMessage({ type: "updateCondensingPrompt", text: customCondensingPrompt || "" }) vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting }) vscode.postMessage({ type: "debugSetting", bool: cachedState.debug }) @@ -900,10 +898,6 @@ const SettingsView = forwardRef(({ onDone, t - setCachedStateField("customCondensingPrompt", value) - } includeTaskHistoryInEnhance={includeTaskHistoryInEnhance} setIncludeTaskHistoryInEnhance={(value) => setCachedStateField("includeTaskHistoryInEnhance", value) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index fa0befd321..fd6a4e1b12 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -58,8 +58,6 @@ export interface ExtensionStateContextType extends ExtensionState { setFollowupAutoApproveTimeoutMs: (value: number) => void // Setter for the timeout condensingApiConfigId?: string setCondensingApiConfigId: (value: string) => void - customCondensingPrompt?: string - setCustomCondensingPrompt: (value: string) => void marketplaceItems?: any[] marketplaceInstalledMetadata?: MarketplaceInstalledMetadata profileThresholds: Record @@ -235,7 +233,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode experiments: experimentDefault, enhancementApiConfigId: "", condensingApiConfigId: "", // Default empty string for condensing API config ID - customCondensingPrompt: "", // Default empty string for custom condensing prompt hasOpenedModeSelector: false, // Default to false (not opened yet) autoApprovalEnabled: false, customModes: [], @@ -456,11 +453,12 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode } // Keep UI semantics consistent with extension: newest-first ordering. nextHistory.sort((a, b) => b.ts - a.ts) - return { - ...prevState, - taskHistory: nextHistory, - currentTaskItem: prevState.currentTaskItem?.id === item.id ? item : prevState.currentTaskItem, - } + return { + ...prevState, + taskHistory: nextHistory, + currentTaskItem: + prevState.currentTaskItem?.id === item.id ? item : prevState.currentTaskItem, + } }) break } @@ -619,8 +617,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setAutoCondenseContextPercent: (value) => setState((prevState) => ({ ...prevState, autoCondenseContextPercent: value })), setCondensingApiConfigId: (value) => setState((prevState) => ({ ...prevState, condensingApiConfigId: value })), - setCustomCondensingPrompt: (value) => - setState((prevState) => ({ ...prevState, customCondensingPrompt: value })), setProfileThresholds: (value) => setState((prevState) => ({ ...prevState, profileThresholds: value })), includeDiagnosticMessages: state.includeDiagnosticMessages, setIncludeDiagnosticMessages: (value) => {