Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
41 changes: 2 additions & 39 deletions src/core/condense/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions src/core/config/ContextProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<string>("customCondensingPrompt")
if (legacyPrompt) {
const currentSupportPrompts =
this.originalContext.globalState.get<Record<string, string>>("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
Expand Down
13 changes: 8 additions & 5 deletions src/core/config/__tests__/ContextProxy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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 () => {
Expand Down
4 changes: 2 additions & 2 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1573,7 +1573,7 @@ export class Task extends EventEmitter<TaskEvents> 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

Expand Down Expand Up @@ -3824,7 +3824,7 @@ export class Task extends EventEmitter<TaskEvents> 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

Expand Down
10 changes: 0 additions & 10 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
50 changes: 8 additions & 42 deletions webview-ui/src/components/settings/PromptsSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,13 @@ import { SearchableSetting } from "./SearchableSetting"
interface PromptsSettingsProps {
customSupportPrompts: Record<string, string | undefined>
setCustomSupportPrompts: (prompts: Record<string, string | undefined>) => void
customCondensingPrompt?: string
setCustomCondensingPrompt?: (value: string) => void
includeTaskHistoryInEnhance?: boolean
setIncludeTaskHistoryInEnhance?: (value: boolean) => void
}

const PromptsSettings = ({
customSupportPrompts,
setCustomSupportPrompts,
customCondensingPrompt: propsCustomCondensingPrompt,
setCustomCondensingPrompt: propsSetCustomCondensingPrompt,
includeTaskHistoryInEnhance: propsIncludeTaskHistoryInEnhance,
setIncludeTaskHistoryInEnhance: propsSetIncludeTaskHistoryInEnhance,
}: PromptsSettingsProps) => {
Expand All @@ -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
Expand Down Expand Up @@ -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)
}

Expand Down
6 changes: 0 additions & 6 deletions webview-ui/src/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
terminalCompressProgressBar,
maxConcurrentFileReads,
condensingApiConfigId,
customCondensingPrompt,
customSupportPrompts,
profileThresholds,
alwaysAllowFollowupQuestions,
Expand Down Expand Up @@ -438,7 +437,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ 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 })
Expand Down Expand Up @@ -900,10 +898,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
<PromptsSettings
customSupportPrompts={customSupportPrompts || {}}
setCustomSupportPrompts={setCustomSupportPromptsField}
customCondensingPrompt={customCondensingPrompt}
setCustomCondensingPrompt={(value) =>
setCachedStateField("customCondensingPrompt", value)
}
includeTaskHistoryInEnhance={includeTaskHistoryInEnhance}
setIncludeTaskHistoryInEnhance={(value) =>
setCachedStateField("includeTaskHistoryInEnhance", value)
Expand Down
16 changes: 6 additions & 10 deletions webview-ui/src/context/ExtensionStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>
Expand Down Expand Up @@ -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: [],
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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) => {
Expand Down
Loading