diff --git a/src/core/Cline.ts b/src/core/Cline.ts index fa74e51ddf6..01b7482903a 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -86,10 +86,11 @@ import { parseMentions } from "./mentions" import { FileContextTracker } from "./context-tracking/FileContextTracker" import { RooIgnoreController } from "./ignore/RooIgnoreController" import { type AssistantMessageContent, parseAssistantMessage } from "./assistant-message" -import { truncateConversationIfNeeded } from "./sliding-window" +import { truncateConversationIfNeeded, estimateTokenCount } from "./sliding-window" // Added estimateTokenCount import { ClineProvider } from "./webview/ClineProvider" import { validateToolUse } from "./mode-validator" import { MultiSearchReplaceDiffStrategy } from "./diff/strategies/multi-search-replace" +import { ContextSynthesizer } from "../services/synthesization/ContextSynthesizer" // Updated to ContextSynthesizer import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "./task-persistence" type UserContent = Array @@ -125,6 +126,11 @@ export type ClineOptions = { parentTask?: Cline taskNumber?: number onCreated?: (cline: Cline) => void + // Context Synthesization Settings + enableContextSummarization?: boolean // Already added + contextSummarizationTriggerThreshold?: number // Already added + contextSummarizationInitialStaticTurns?: number // Already added + contextSummarizationRecentTurns?: number // Already added } export class Cline extends EventEmitter { @@ -154,6 +160,12 @@ export class Cline extends EventEmitter { diffEnabled: boolean = false fuzzyMatchThreshold: number + // Context Synthesization Settings (Added) + readonly enableContextSummarization: boolean + readonly contextSummarizationTriggerThreshold: number + readonly contextSummarizationInitialStaticTurns: number + readonly contextSummarizationRecentTurns: number + apiConversationHistory: (Anthropic.MessageParam & { ts?: number })[] = [] clineMessages: ClineMessage[] = [] @@ -217,6 +229,11 @@ export class Cline extends EventEmitter { parentTask, taskNumber = -1, onCreated, + // Context Synthesization Settings (Added) + enableContextSummarization = false, + contextSummarizationTriggerThreshold = 80, + contextSummarizationInitialStaticTurns = 3, // Changed default from 5 to 3 + contextSummarizationRecentTurns = 3, // Changed default from 10 to 3 }: ClineOptions) { super() @@ -250,6 +267,11 @@ export class Cline extends EventEmitter { this.diffViewProvider = new DiffViewProvider(this.cwd) this.enableCheckpoints = enableCheckpoints + this.enableContextSummarization = enableContextSummarization + this.contextSummarizationTriggerThreshold = contextSummarizationTriggerThreshold + this.contextSummarizationInitialStaticTurns = contextSummarizationInitialStaticTurns + this.contextSummarizationRecentTurns = contextSummarizationRecentTurns + this.rootTask = rootTask this.parentTask = parentTask this.taskNumber = taskNumber @@ -348,7 +370,7 @@ export class Cline extends EventEmitter { this.emit("message", { action: "updated", message: partialMessage }) } - private async saveClineMessages() { + public async saveClineMessages() { try { await saveTaskMessages({ messages: this.clineMessages, @@ -1011,7 +1033,8 @@ export class Cline extends EventEmitter { ) })() - // If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request + // If the previous API request's total token usage is close to the context window, + // either truncate or summarize the conversation history based on settings. if (previousApiReqIndex >= 0) { const previousRequest = this.clineMessages[previousApiReqIndex]?.text @@ -1020,6 +1043,7 @@ export class Cline extends EventEmitter { } const { + // These tokens are from the *previous* request's response, not the current history size tokensIn = 0, tokensOut = 0, cacheWrites = 0, @@ -1038,17 +1062,102 @@ export class Cline extends EventEmitter { : modelInfo.maxTokens const contextWindow = modelInfo.contextWindow + let historyModifiedBySynthesization = false // Flag to track if synthesization updated history + + if (this.enableContextSummarization) { + // --- Synthesizing Logic --- + const currentTokens = await this._estimateTotalTokenCount(this.apiConversationHistory) + const triggerTokenCount = contextWindow * (this.contextSummarizationTriggerThreshold / 100) + + this.providerRef + .deref() + ?.log(`[Synthesizing] Current tokens: ${currentTokens}, Trigger: ${triggerTokenCount}`) + + if (currentTokens >= triggerTokenCount) { + this.providerRef.deref()?.log(`[Synthesizing] Threshold met. Attempting synthesizing.`) + const initialMessagesToKeep = this.contextSummarizationInitialStaticTurns + const recentMessagesToKeep = this.contextSummarizationRecentTurns + + // Ensure slice points are valid and don't overlap negatively + if ( + this.apiConversationHistory.length > initialMessagesToKeep + recentMessagesToKeep && + initialMessagesToKeep >= 0 && + recentMessagesToKeep >= 0 + ) { + const initialSliceEnd = initialMessagesToKeep + const recentSliceStart = this.apiConversationHistory.length - recentMessagesToKeep + + if (initialSliceEnd < recentSliceStart) { + const initialMessages = this.apiConversationHistory.slice(0, initialSliceEnd) + const recentMessages = this.apiConversationHistory.slice(recentSliceStart) + const messagesToSynthesize = this.apiConversationHistory.slice( + initialSliceEnd, + recentSliceStart, + ) - const trimmedMessages = await truncateConversationIfNeeded({ - messages: this.apiConversationHistory, - totalTokens, - maxTokens, - contextWindow, - apiHandler: this.api, - }) + this.providerRef + .deref() + ?.log( + `[Synthesizing] Slicing: Keep Initial ${initialMessages.length}, Synthesize ${messagesToSynthesize.length}, Keep Recent ${recentMessages.length}`, + ) + + // Instantiate the synthesizer (consider using a dedicated API handler/model later) + const synthesizer = new ContextSynthesizer(this.api) + const summaryMessage = await synthesizer.synthesize(messagesToSynthesize) + + if (summaryMessage) { + const newHistory = [...initialMessages, summaryMessage, ...recentMessages] + this.providerRef + .deref() + ?.log( + `[Synthesizing] Synthesizing successful. New history length: ${newHistory.length}`, + ) + // Add a system message to notify the user in the UI + await this.say("text", "[Older conversation turns synthesized to preserve context]") + await this.overwriteApiConversationHistory(newHistory) + historyModifiedBySynthesization = true // Mark history as modified + } else { + this.providerRef + .deref() + ?.log(`[Synthesizing] Synthesizing failed. Falling back to truncation.`) + // Fall through to truncation if synthesization fails + } + } else { + this.providerRef + .deref() + ?.log( + `[Synthesizing] Skipping: initialSliceEnd (${initialSliceEnd}) >= recentSliceStart (${recentSliceStart}). Not enough messages between initial/recent turns.`, + ) + // Fall through to truncation if slicing is not possible + } + } else { + this.providerRef + .deref() + ?.log( + `[Synthesizing] Skipping: Not enough messages (${this.apiConversationHistory.length}) to satisfy keep counts (${initialMessagesToKeep} + ${recentMessagesToKeep}).`, + ) + // Fall through to truncation if history is too short + } + } + // If synthesization is enabled but threshold not met, do nothing and proceed. + } + + // --- Truncation Logic (Only run if synthesization didn't modify history) --- + if (!historyModifiedBySynthesization) { + // Note: totalTokens here refers to the previous response size, used by truncateConversationIfNeeded + // to estimate if the *next* request might overflow. + const trimmedMessages = await truncateConversationIfNeeded({ + messages: this.apiConversationHistory, // Use potentially already summarized history if synthesization failed above + totalTokens, // From previous response metrics + maxTokens, + contextWindow, + apiHandler: this.api, + }) - if (trimmedMessages !== this.apiConversationHistory) { - await this.overwriteApiConversationHistory(trimmedMessages) + if (trimmedMessages !== this.apiConversationHistory) { + this.providerRef.deref()?.log(`[Synthesizing] Truncation applied as fallback.`) + await this.overwriteApiConversationHistory(trimmedMessages) + } } } @@ -2566,4 +2675,88 @@ export class Cline extends EventEmitter { public getToolUsage() { return this.toolUsage } + + /** + * Estimates the total token count for an array of messages. + * @param messages The messages to count tokens for. + * @returns A promise resolving to the estimated total token count. + */ + private async _estimateTotalTokenCount(messages: Anthropic.MessageParam[]): Promise { + let totalTokens = 0 + for (const message of messages) { + const content = message.content + if (Array.isArray(content)) { + totalTokens += await estimateTokenCount(content, this.api) + } else if (typeof content === "string") { + totalTokens += await estimateTokenCount([{ type: "text", text: content }], this.api) + } + } + return totalTokens + } + + /** + * Manually triggers synthesization of the conversation context. + * @param isManualTrigger Whether this synthesization was manually triggered by the user. + * @returns A promise that resolves when synthesization is complete. + */ + public async synthesizeConversationContext(_isManualTrigger: boolean = false): Promise { + const initialMessagesToKeep = this.contextSummarizationInitialStaticTurns + const recentMessagesToKeep = this.contextSummarizationRecentTurns + + // Ensure we have enough messages to synthesize + if (this.apiConversationHistory.length <= initialMessagesToKeep + recentMessagesToKeep) { + this.providerRef + .deref() + ?.log( + `[Synthesizing] Not enough messages to synthesize. Need more than ${initialMessagesToKeep + recentMessagesToKeep} messages.`, + ) + return + } + + // Calculate slice points + const initialSliceEnd = initialMessagesToKeep + const recentSliceStart = this.apiConversationHistory.length - recentMessagesToKeep + + // Ensure slice points don't overlap + if (initialSliceEnd >= recentSliceStart) { + this.providerRef + .deref() + ?.log( + `[Synthesizing] Skipping: initialSliceEnd (${initialSliceEnd}) >= recentSliceStart (${recentSliceStart}). Not enough messages between initial/recent turns.`, + ) + return + } + + // Slice the conversation history + const initialMessages = this.apiConversationHistory.slice(0, initialSliceEnd) + const recentMessages = this.apiConversationHistory.slice(recentSliceStart) + const messagesToSynthesize = this.apiConversationHistory.slice(initialSliceEnd, recentSliceStart) + + this.providerRef + .deref() + ?.log( + `[Synthesizing] Slicing: Keep Initial ${initialMessages.length}, Synthesize ${messagesToSynthesize.length}, Keep Recent ${recentMessages.length}`, + ) + + // Create synthesizer and generate synthesis + const synthesizer = new ContextSynthesizer(this.api) + const summaryMessage = await synthesizer.synthesize(messagesToSynthesize) + + if (!summaryMessage) { + this.providerRef.deref()?.log(`[Synthesizing] Failed to generate synthesis.`) + return + } + + // Create new history with summary + const newHistory = [...initialMessages, summaryMessage, ...recentMessages] + + // Update the conversation history + await this.overwriteApiConversationHistory(newHistory) + + this.providerRef + .deref() + ?.log( + `[Synthesizing] Successfully synthesized ${messagesToSynthesize.length} messages. New history length: ${newHistory.length}`, + ) + } } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 86e3b9d64d5..78aba1af15b 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -494,6 +494,11 @@ export class ClineProvider extends EventEmitter implements mode, customInstructions: globalInstructions, experiments, + // Context Synthesization Settings (Added) + enableContextSummarization, + contextSummarizationTriggerThreshold, + contextSummarizationInitialStaticTurns, + contextSummarizationRecentTurns, } = await this.getState() const modePrompt = customModePrompts?.[mode] as PromptComponent @@ -513,6 +518,11 @@ export class ClineProvider extends EventEmitter implements parentTask, taskNumber: this.clineStack.length + 1, onCreated: (cline) => this.emit("clineCreated", cline), + // Pass synthesization settings to Cline (Added) + enableContextSummarization, + contextSummarizationTriggerThreshold, + contextSummarizationInitialStaticTurns, + contextSummarizationRecentTurns, ...options, }) @@ -537,6 +547,11 @@ export class ClineProvider extends EventEmitter implements mode, customInstructions: globalInstructions, experiments, + // Context Synthesization Settings (Added) + enableContextSummarization, + contextSummarizationTriggerThreshold, + contextSummarizationInitialStaticTurns, + contextSummarizationRecentTurns, } = await this.getState() const modePrompt = customModePrompts?.[mode] as PromptComponent @@ -555,6 +570,11 @@ export class ClineProvider extends EventEmitter implements parentTask: historyItem.parentTask, taskNumber: historyItem.number, onCreated: (cline) => this.emit("clineCreated", cline), + // Pass synthesization settings to Cline (Added) + enableContextSummarization, + contextSummarizationTriggerThreshold, + contextSummarizationInitialStaticTurns, + contextSummarizationRecentTurns, }) await this.addClineToStack(cline) @@ -1215,6 +1235,11 @@ export class ClineProvider extends EventEmitter implements maxReadFileLine, terminalCompressProgressBar, historyPreviewCollapsed, + // Context Synthesization Settings (Added) + enableContextSummarization, + contextSummarizationTriggerThreshold, + contextSummarizationInitialStaticTurns, + contextSummarizationRecentTurns, } = await this.getState() const telemetryKey = process.env.POSTHOG_API_KEY @@ -1302,6 +1327,11 @@ export class ClineProvider extends EventEmitter implements terminalCompressProgressBar: terminalCompressProgressBar ?? true, hasSystemPromptOverride, historyPreviewCollapsed: historyPreviewCollapsed ?? false, + // Context Synthesization Settings (Added) + enableContextSummarization: enableContextSummarization ?? false, + contextSummarizationTriggerThreshold: contextSummarizationTriggerThreshold ?? 80, + contextSummarizationInitialStaticTurns: contextSummarizationInitialStaticTurns ?? 3, // Changed default from 5 to 3 + contextSummarizationRecentTurns: contextSummarizationRecentTurns ?? 3, // Changed default from 10 to 3 } } @@ -1392,6 +1422,11 @@ export class ClineProvider extends EventEmitter implements showRooIgnoredFiles: stateValues.showRooIgnoredFiles ?? true, maxReadFileLine: stateValues.maxReadFileLine ?? 500, historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false, + // Context Synthesization Settings (Added) + enableContextSummarization: stateValues.enableContextSummarization ?? false, + contextSummarizationTriggerThreshold: stateValues.contextSummarizationTriggerThreshold ?? 80, + contextSummarizationInitialStaticTurns: stateValues.contextSummarizationInitialStaticTurns ?? 3, // Changed default from 5 to 3 + contextSummarizationRecentTurns: stateValues.contextSummarizationRecentTurns ?? 3, // Changed default from 10 to 3 } } diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index 2942bc43b11..c966104c07c 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -425,6 +425,11 @@ describe("ClineProvider", () => { showRooIgnoredFiles: true, renderContext: "sidebar", maxReadFileLine: 500, + // Context Synthesization Defaults (Added for test) + enableContextSummarization: false, + contextSummarizationTriggerThreshold: 80, + contextSummarizationInitialStaticTurns: 5, + contextSummarizationRecentTurns: 10, } const message: ExtensionMessage = { @@ -729,6 +734,68 @@ describe("ClineProvider", () => { expect((await provider.getState()).showRooIgnoredFiles).toBe(false) }) + test("handles context synthesization settings messages", async () => { + await provider.resolveWebviewView(mockWebviewView) + const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] + + // Test enableContextSummarization + await messageHandler({ type: "enableContextSummarization", bool: true }) + expect(updateGlobalStateSpy).toHaveBeenCalledWith("enableContextSummarization", true) + expect(mockContext.globalState.update).toHaveBeenCalledWith("enableContextSummarization", true) + expect(mockPostMessage).toHaveBeenCalled() + expect((await provider.getState()).enableContextSummarization).toBe(true) + + await messageHandler({ type: "enableContextSummarization", bool: false }) + expect(updateGlobalStateSpy).toHaveBeenCalledWith("enableContextSummarization", false) + expect(mockContext.globalState.update).toHaveBeenCalledWith("enableContextSummarization", false) + expect(mockPostMessage).toHaveBeenCalled() + expect((await provider.getState()).enableContextSummarization).toBe(false) + + // Test contextSummarizationTriggerThreshold + await messageHandler({ type: "contextSummarizationTriggerThreshold", value: 90 }) + expect(updateGlobalStateSpy).toHaveBeenCalledWith("contextSummarizationTriggerThreshold", 90) + expect(mockContext.globalState.update).toHaveBeenCalledWith("contextSummarizationTriggerThreshold", 90) + expect(mockPostMessage).toHaveBeenCalled() + expect((await provider.getState()).contextSummarizationTriggerThreshold).toBe(90) + + // Test contextSummarizationInitialStaticTurns + await messageHandler({ type: "contextSummarizationInitialStaticTurns", value: 3 }) + expect(updateGlobalStateSpy).toHaveBeenCalledWith("contextSummarizationInitialStaticTurns", 3) + expect(mockContext.globalState.update).toHaveBeenCalledWith("contextSummarizationInitialStaticTurns", 3) + expect(mockPostMessage).toHaveBeenCalled() + expect((await provider.getState()).contextSummarizationInitialStaticTurns).toBe(3) + + // Test contextSummarizationRecentTurns + await messageHandler({ type: "contextSummarizationRecentTurns", value: 15 }) + expect(updateGlobalStateSpy).toHaveBeenCalledWith("contextSummarizationRecentTurns", 15) + expect(mockContext.globalState.update).toHaveBeenCalledWith("contextSummarizationRecentTurns", 15) + expect(mockPostMessage).toHaveBeenCalled() + expect((await provider.getState()).contextSummarizationRecentTurns).toBe(15) + }) + + test("context synthesization settings have correct default values", async () => { + // Mock globalState.get to return undefined for the new settings + ;(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => { + if ( + [ + "enableContextSummarization", + "contextSummarizationTriggerThreshold", + "contextSummarizationInitialStaticTurns", + "contextSummarizationRecentTurns", + ].includes(key) + ) { + return undefined + } + return null // Return null for other keys to avoid interference + }) + + const state = await provider.getState() + expect(state.enableContextSummarization).toBe(false) + expect(state.contextSummarizationTriggerThreshold).toBe(80) + expect(state.contextSummarizationInitialStaticTurns).toBe(5) + expect(state.contextSummarizationRecentTurns).toBe(10) + }) + test("handles request delay settings messages", async () => { await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 94bd68f9c94..74956491d24 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -860,6 +860,60 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We await updateGlobalState("historyPreviewCollapsed", message.bool ?? false) // No need to call postStateToWebview here as the UI already updated optimistically break + // Context Synthesization Settings + case "enableContextSummarization": + await updateGlobalState("enableContextSummarization", message.bool ?? false) + await provider.postStateToWebview() + break + case "contextSummarizationTriggerThreshold": + await updateGlobalState("contextSummarizationTriggerThreshold", message.value ?? 80) + await provider.postStateToWebview() + break + case "contextSummarizationInitialStaticTurns": + await updateGlobalState("contextSummarizationInitialStaticTurns", message.value ?? 5) + await provider.postStateToWebview() + break + case "contextSummarizationRecentTurns": + await updateGlobalState("contextSummarizationRecentTurns", message.value ?? 10) + await provider.postStateToWebview() + break + case "manualSynthesize": + // Trigger manual synthesizing of the conversation context + const currentCline = provider.getCurrentCline() + if (currentCline) { + // First send a message to the webview to show a progress indicator + void provider.postMessageToWebview({ + type: "synthesizationStatus", + status: "started", + text: t("common:info.synthesizing_context"), + }) + + // Use a non-blocking approach with proper error handling + try { + // Trigger the synthesizing process directly without adding system messages + await currentCline.synthesizeConversationContext(true) // true indicates manual trigger + + // Send a message to the webview to hide the progress indicator + void provider.postMessageToWebview({ + type: "synthesizationStatus", + status: "completed", + text: t("common:info.synthesization_complete"), + }) + } catch (error) { + provider.log( + `Error during manual synthesizing: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`, + ) + + // Update the UI to show the error + void provider.postMessageToWebview({ + type: "synthesizationStatus", + status: "failed", + text: t("common:errors.synthesization_failed"), + }) + } + } + break + // --- End Context Synthesization --- case "toggleApiConfigPin": if (message.text) { const currentPinned = getGlobalState("pinnedApiConfigs") ?? {} diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index 2c552b17021..fd71f3ee509 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -217,6 +217,10 @@ type GlobalSettings = { terminalZshP10k?: boolean | undefined terminalZdotdir?: boolean | undefined terminalCompressProgressBar?: boolean | undefined + enableContextSummarization?: boolean | undefined + contextSummarizationTriggerThreshold?: number | undefined + contextSummarizationInitialStaticTurns?: number | undefined + contextSummarizationRecentTurns?: number | undefined rateLimitSeconds?: number | undefined diffEnabled?: boolean | undefined fuzzyMatchThreshold?: number | undefined @@ -333,6 +337,7 @@ type ClineMessage = { | "checkpoint_saved" | "rooignore_error" | "diff_error" + | "summarizing" ) | undefined text?: string | undefined @@ -408,6 +413,7 @@ type RooCodeEvents = { | "checkpoint_saved" | "rooignore_error" | "diff_error" + | "summarizing" ) | undefined text?: string | undefined diff --git a/src/exports/types.ts b/src/exports/types.ts index 10724695098..775e53f3e70 100644 --- a/src/exports/types.ts +++ b/src/exports/types.ts @@ -220,6 +220,10 @@ type GlobalSettings = { terminalZshP10k?: boolean | undefined terminalZdotdir?: boolean | undefined terminalCompressProgressBar?: boolean | undefined + enableContextSummarization?: boolean | undefined + contextSummarizationTriggerThreshold?: number | undefined + contextSummarizationInitialStaticTurns?: number | undefined + contextSummarizationRecentTurns?: number | undefined rateLimitSeconds?: number | undefined diffEnabled?: boolean | undefined fuzzyMatchThreshold?: number | undefined @@ -338,6 +342,7 @@ type ClineMessage = { | "checkpoint_saved" | "rooignore_error" | "diff_error" + | "summarizing" ) | undefined text?: string | undefined @@ -417,6 +422,7 @@ type RooCodeEvents = { | "checkpoint_saved" | "rooignore_error" | "diff_error" + | "summarizing" ) | undefined text?: string | undefined diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index 633b90ec3d0..94015ac78c1 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -55,7 +55,8 @@ "failed_delete_repo": "Ha fallat l'eliminació del repositori o branca associada: {{error}}", "failed_remove_directory": "Ha fallat l'eliminació del directori de tasques: {{error}}", "custom_storage_path_unusable": "La ruta d'emmagatzematge personalitzada \"{{path}}\" no és utilitzable, s'utilitzarà la ruta predeterminada", - "cannot_access_path": "No es pot accedir a la ruta {{path}}: {{error}}" + "cannot_access_path": "No es pot accedir a la ruta {{path}}: {{error}}", + "synthesization_failed": "Ha fallat la síntesi del context de la conversa." }, "warnings": { "no_terminal_content": "No s'ha seleccionat contingut de terminal", @@ -71,7 +72,9 @@ "mcp_server_not_found": "Servidor \"{{serverName}}\" no trobat a la configuració", "custom_storage_path_set": "Ruta d'emmagatzematge personalitzada establerta: {{path}}", "default_storage_path": "S'ha reprès l'ús de la ruta d'emmagatzematge predeterminada", - "settings_imported": "Configuració importada correctament." + "settings_imported": "Configuració importada correctament.", + "synthesizing_context": "Sintetitzant el context de la conversa...", + "synthesization_complete": "Síntesi del context de la conversa completada." }, "answers": { "yes": "Sí", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index ceda64bad71..5f4ec30596a 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -51,7 +51,8 @@ "failed_delete_repo": "Fehler beim Löschen des zugehörigen Shadow-Repositorys oder -Zweigs: {{error}}", "failed_remove_directory": "Fehler beim Entfernen des Aufgabenverzeichnisses: {{error}}", "custom_storage_path_unusable": "Benutzerdefinierter Speicherpfad \"{{path}}\" ist nicht verwendbar, Standardpfad wird verwendet", - "cannot_access_path": "Zugriff auf Pfad {{path}} nicht möglich: {{error}}" + "cannot_access_path": "Zugriff auf Pfad {{path}} nicht möglich: {{error}}", + "synthesization_failed": "Synthese des Gesprächskontexts fehlgeschlagen." }, "warnings": { "no_terminal_content": "Kein Terminal-Inhalt ausgewählt", @@ -67,7 +68,9 @@ "mcp_server_not_found": "Server \"{{serverName}}\" nicht in der Konfiguration gefunden", "custom_storage_path_set": "Benutzerdefinierter Speicherpfad festgelegt: {{path}}", "default_storage_path": "Auf Standardspeicherpfad zurückgesetzt", - "settings_imported": "Einstellungen erfolgreich importiert." + "settings_imported": "Einstellungen erfolgreich importiert.", + "synthesizing_context": "Gesprächskontext wird synthetisiert...", + "synthesization_complete": "Synthese des Gesprächskontexts abgeschlossen." }, "answers": { "yes": "Ja", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index ecd5a4c4133..a5ec6bccca7 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -47,6 +47,7 @@ "create_mcp_json": "Failed to create or open .roo/mcp.json: {{error}}", "hmr_not_running": "Local development server is not running, HMR will not work. Please run 'npm run dev' before launching the extension to enable HMR.", "retrieve_current_mode": "Error: failed to retrieve current mode from state.", + "synthesization_failed": "Failed to synthesize conversation context.", "failed_delete_repo": "Failed to delete associated shadow repository or branch: {{error}}", "failed_remove_directory": "Failed to remove task directory: {{error}}", "custom_storage_path_unusable": "Custom storage path \"{{path}}\" is unusable, will use default path", @@ -67,7 +68,9 @@ "mcp_server_not_found": "Server \"{{serverName}}\" not found in configuration", "custom_storage_path_set": "Custom storage path set: {{path}}", "default_storage_path": "Reverted to using default storage path", - "settings_imported": "Settings imported successfully." + "settings_imported": "Settings imported successfully.", + "synthesizing_context": "Synthesizing conversation context...", + "synthesization_complete": "Conversation context synthesization complete." }, "answers": { "yes": "Yes", diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 2bfb43055a8..d0f58a748a3 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -51,7 +51,8 @@ "failed_delete_repo": "Error al eliminar el repositorio o rama asociada: {{error}}", "failed_remove_directory": "Error al eliminar el directorio de tareas: {{error}}", "custom_storage_path_unusable": "La ruta de almacenamiento personalizada \"{{path}}\" no es utilizable, se usará la ruta predeterminada", - "cannot_access_path": "No se puede acceder a la ruta {{path}}: {{error}}" + "cannot_access_path": "No se puede acceder a la ruta {{path}}: {{error}}", + "synthesization_failed": "Error al sintetizar el contexto de la conversación." }, "warnings": { "no_terminal_content": "No hay contenido de terminal seleccionado", @@ -67,7 +68,9 @@ "mcp_server_not_found": "Servidor \"{{serverName}}\" no encontrado en la configuración", "custom_storage_path_set": "Ruta de almacenamiento personalizada establecida: {{path}}", "default_storage_path": "Se ha vuelto a usar la ruta de almacenamiento predeterminada", - "settings_imported": "Configuración importada correctamente." + "settings_imported": "Configuración importada correctamente.", + "synthesizing_context": "Sintetizando el contexto de la conversación...", + "synthesization_complete": "Síntesis del contexto de la conversación completada." }, "answers": { "yes": "Sí", diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index 7399432c6a8..00cb6f6ecc2 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -51,7 +51,8 @@ "failed_delete_repo": "Échec de la suppression du repo fantôme ou de la branche associée : {{error}}", "failed_remove_directory": "Échec de la suppression du répertoire de tâches : {{error}}", "custom_storage_path_unusable": "Le chemin de stockage personnalisé \"{{path}}\" est inutilisable, le chemin par défaut sera utilisé", - "cannot_access_path": "Impossible d'accéder au chemin {{path}} : {{error}}" + "cannot_access_path": "Impossible d'accéder au chemin {{path}} : {{error}}", + "synthesization_failed": "Échec de la synthèse du contexte de la conversation." }, "warnings": { "no_terminal_content": "Aucun contenu de terminal sélectionné", @@ -67,7 +68,9 @@ "mcp_server_not_found": "Serveur \"{{serverName}}\" introuvable dans la configuration", "custom_storage_path_set": "Chemin de stockage personnalisé défini : {{path}}", "default_storage_path": "Retour au chemin de stockage par défaut", - "settings_imported": "Paramètres importés avec succès." + "settings_imported": "Paramètres importés avec succès.", + "synthesizing_context": "Synthèse du contexte de la conversation en cours...", + "synthesization_complete": "Synthèse du contexte de la conversation terminée." }, "answers": { "yes": "Oui", diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index bf5421eff8f..700ab9ecc4a 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -51,7 +51,8 @@ "failed_delete_repo": "संबंधित शैडो रिपॉजिटरी या ब्रांच हटाने में विफल: {{error}}", "failed_remove_directory": "टास्क डायरेक्टरी हटाने में विफल: {{error}}", "custom_storage_path_unusable": "कस्टम स्टोरेज पाथ \"{{path}}\" उपयोग योग्य नहीं है, डिफ़ॉल्ट पाथ का उपयोग किया जाएगा", - "cannot_access_path": "पाथ {{path}} तक पहुंच नहीं पा रहे हैं: {{error}}" + "cannot_access_path": "पाथ {{path}} तक पहुंच नहीं पा रहे हैं: {{error}}", + "synthesization_failed": "बातचीत के संदर्भ को संश्लेषित करने में विफल।" }, "warnings": { "no_terminal_content": "कोई टर्मिनल सामग्री चयनित नहीं", @@ -67,7 +68,9 @@ "mcp_server_not_found": "सर्वर \"{{serverName}}\" कॉन्फ़िगरेशन में नहीं मिला", "custom_storage_path_set": "कस्टम स्टोरेज पाथ सेट किया गया: {{path}}", "default_storage_path": "डिफ़ॉल्ट स्टोरेज पाथ का उपयोग पुनः शुरू किया गया", - "settings_imported": "सेटिंग्स सफलतापूर्वक इम्पोर्ट की गईं।" + "settings_imported": "सेटिंग्स सफलतापूर्वक इम्पोर्ट की गईं।", + "synthesizing_context": "बातचीत के संदर्भ को संश्लेषित किया जा रहा है...", + "synthesization_complete": "बातचीत के संदर्भ का संश्लेषण पूरा हुआ।" }, "answers": { "yes": "हां", diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index 69e2c2123f1..99eb4ce654f 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -51,7 +51,8 @@ "failed_delete_repo": "Impossibile eliminare il repository o il ramo associato: {{error}}", "failed_remove_directory": "Impossibile rimuovere la directory delle attività: {{error}}", "custom_storage_path_unusable": "Il percorso di archiviazione personalizzato \"{{path}}\" non è utilizzabile, verrà utilizzato il percorso predefinito", - "cannot_access_path": "Impossibile accedere al percorso {{path}}: {{error}}" + "cannot_access_path": "Impossibile accedere al percorso {{path}}: {{error}}", + "synthesization_failed": "Sintesi del contesto della conversazione fallita." }, "warnings": { "no_terminal_content": "Nessun contenuto del terminale selezionato", @@ -67,7 +68,9 @@ "mcp_server_not_found": "Server \"{{serverName}}\" non trovato nella configurazione", "custom_storage_path_set": "Percorso di archiviazione personalizzato impostato: {{path}}", "default_storage_path": "Tornato al percorso di archiviazione predefinito", - "settings_imported": "Impostazioni importate con successo." + "settings_imported": "Impostazioni importate con successo.", + "synthesizing_context": "Sintesi del contesto della conversazione in corso...", + "synthesization_complete": "Sintesi del contesto della conversazione completata." }, "answers": { "yes": "Sì", diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index 6f40c8e03d5..6606f6ac7cb 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -51,7 +51,8 @@ "failed_delete_repo": "関連するシャドウリポジトリまたはブランチの削除に失敗しました:{{error}}", "failed_remove_directory": "タスクディレクトリの削除に失敗しました:{{error}}", "custom_storage_path_unusable": "カスタムストレージパス \"{{path}}\" が使用できないため、デフォルトパスを使用します", - "cannot_access_path": "パス {{path}} にアクセスできません:{{error}}" + "cannot_access_path": "パス {{path}} にアクセスできません:{{error}}", + "synthesization_failed": "会話コンテキストの合成に失敗しました。" }, "warnings": { "no_terminal_content": "選択されたターミナルコンテンツがありません", @@ -67,7 +68,9 @@ "mcp_server_not_found": "サーバー\"{{serverName}}\"が設定内に見つかりません", "custom_storage_path_set": "カスタムストレージパスが設定されました:{{path}}", "default_storage_path": "デフォルトのストレージパスに戻りました", - "settings_imported": "設定が正常にインポートされました。" + "settings_imported": "設定が正常にインポートされました。", + "synthesizing_context": "会話コンテキストを合成中...", + "synthesization_complete": "会話コンテキストの合成が完了しました。" }, "answers": { "yes": "はい", diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 9026315da2e..5ef6a11cd61 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -51,7 +51,8 @@ "failed_delete_repo": "관련 shadow 저장소 또는 브랜치 삭제 실패: {{error}}", "failed_remove_directory": "작업 디렉토리 제거 실패: {{error}}", "custom_storage_path_unusable": "사용자 지정 저장 경로 \"{{path}}\"를 사용할 수 없어 기본 경로를 사용합니다", - "cannot_access_path": "경로 {{path}}에 접근할 수 없습니다: {{error}}" + "cannot_access_path": "경로 {{path}}에 접근할 수 없습니다: {{error}}", + "synthesization_failed": "대화 컨텍스트 합성에 실패했습니다." }, "warnings": { "no_terminal_content": "선택된 터미널 내용이 없습니다", @@ -67,7 +68,9 @@ "mcp_server_not_found": "구성에서 서버 \"{{serverName}}\"을(를) 찾을 수 없습니다", "custom_storage_path_set": "사용자 지정 저장 경로 설정됨: {{path}}", "default_storage_path": "기본 저장 경로로 되돌아갔습니다", - "settings_imported": "설정이 성공적으로 가져와졌습니다." + "settings_imported": "설정이 성공적으로 가져와졌습니다.", + "synthesizing_context": "대화 컨텍스트 합성 중...", + "synthesization_complete": "대화 컨텍스트 합성이 완료되었습니다." }, "answers": { "yes": "예", diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 49f51cefff1..6e483d5a7b4 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -51,7 +51,8 @@ "failed_delete_repo": "Nie udało się usunąć powiązanego repozytorium lub gałęzi pomocniczej: {{error}}", "failed_remove_directory": "Nie udało się usunąć katalogu zadania: {{error}}", "custom_storage_path_unusable": "Niestandardowa ścieżka przechowywania \"{{path}}\" nie jest użyteczna, zostanie użyta domyślna ścieżka", - "cannot_access_path": "Nie można uzyskać dostępu do ścieżki {{path}}: {{error}}" + "cannot_access_path": "Nie można uzyskać dostępu do ścieżki {{path}}: {{error}}", + "synthesization_failed": "Nie udało się zsyntetyzować kontekstu rozmowy." }, "warnings": { "no_terminal_content": "Nie wybrano zawartości terminala", @@ -67,7 +68,9 @@ "mcp_server_not_found": "Serwer \"{{serverName}}\" nie znaleziony w konfiguracji", "custom_storage_path_set": "Ustawiono niestandardową ścieżkę przechowywania: {{path}}", "default_storage_path": "Wznowiono używanie domyślnej ścieżki przechowywania", - "settings_imported": "Ustawienia zaimportowane pomyślnie." + "settings_imported": "Ustawienia zaimportowane pomyślnie.", + "synthesizing_context": "Syntetyzowanie kontekstu rozmowy...", + "synthesization_complete": "Synteza kontekstu rozmowy zakończona." }, "answers": { "yes": "Tak", diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 80112a91ab8..7ee2eb3c8f0 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -55,7 +55,8 @@ "failed_delete_repo": "Falha ao excluir o repositório ou ramificação associada: {{error}}", "failed_remove_directory": "Falha ao remover o diretório de tarefas: {{error}}", "custom_storage_path_unusable": "O caminho de armazenamento personalizado \"{{path}}\" não pode ser usado, será usado o caminho padrão", - "cannot_access_path": "Não é possível acessar o caminho {{path}}: {{error}}" + "cannot_access_path": "Não é possível acessar o caminho {{path}}: {{error}}", + "synthesization_failed": "Falha ao sintetizar o contexto da conversa." }, "warnings": { "no_terminal_content": "Nenhum conteúdo do terminal selecionado", @@ -71,7 +72,9 @@ "mcp_server_not_found": "Servidor \"{{serverName}}\" não encontrado na configuração", "custom_storage_path_set": "Caminho de armazenamento personalizado definido: {{path}}", "default_storage_path": "Retornado ao caminho de armazenamento padrão", - "settings_imported": "Configurações importadas com sucesso." + "settings_imported": "Configurações importadas com sucesso.", + "synthesizing_context": "Sintetizando o contexto da conversa...", + "synthesization_complete": "Síntese do contexto da conversa concluída." }, "answers": { "yes": "Sim", diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 80829e138ce..37bcf56f4da 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -51,7 +51,8 @@ "failed_remove_directory": "Не удалось удалить директорию задачи: {{error}}", "custom_storage_path_unusable": "Пользовательский путь хранения \"{{path}}\" непригоден, будет использован путь по умолчанию", "cannot_access_path": "Невозможно получить доступ к пути {{path}}: {{error}}", - "failed_update_project_mcp": "Не удалось обновить серверы проекта MCP" + "failed_update_project_mcp": "Не удалось обновить серверы проекта MCP", + "synthesization_failed": "Не удалось синтезировать контекст разговора." }, "warnings": { "no_terminal_content": "Не выбрано содержимое терминала", @@ -67,7 +68,9 @@ "mcp_server_not_found": "Сервер \"{{serverName}}\" не найден в конфигурации", "custom_storage_path_set": "Установлен пользовательский путь хранения: {{path}}", "default_storage_path": "Возвращено использование пути хранения по умолчанию", - "settings_imported": "Настройки успешно импортированы." + "settings_imported": "Настройки успешно импортированы.", + "synthesizing_context": "Синтез контекста разговора...", + "synthesization_complete": "Синтез контекста разговора завершен." }, "answers": { "yes": "Да", diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 61b8e12fb5a..6de563bd22f 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -51,7 +51,8 @@ "failed_delete_repo": "İlişkili gölge depo veya dal silinemedi: {{error}}", "failed_remove_directory": "Görev dizini kaldırılamadı: {{error}}", "custom_storage_path_unusable": "Özel depolama yolu \"{{path}}\" kullanılamıyor, varsayılan yol kullanılacak", - "cannot_access_path": "{{path}} yoluna erişilemiyor: {{error}}" + "cannot_access_path": "{{path}} yoluna erişilemiyor: {{error}}", + "synthesization_failed": "Konuşma bağlamı sentezlenemedi." }, "warnings": { "no_terminal_content": "Seçili terminal içeriği yok", @@ -67,7 +68,9 @@ "mcp_server_not_found": "Yapılandırmada \"{{serverName}}\" sunucusu bulunamadı", "custom_storage_path_set": "Özel depolama yolu ayarlandı: {{path}}", "default_storage_path": "Varsayılan depolama yoluna geri dönüldü", - "settings_imported": "Ayarlar başarıyla içe aktarıldı." + "settings_imported": "Ayarlar başarıyla içe aktarıldı.", + "synthesizing_context": "Konuşma bağlamı sentezleniyor...", + "synthesization_complete": "Konuşma bağlamı sentezi tamamlandı." }, "answers": { "yes": "Evet", diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 8945e9e098e..7e8151e96df 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -51,7 +51,8 @@ "failed_delete_repo": "Không thể xóa kho lưu trữ hoặc nhánh liên quan: {{error}}", "failed_remove_directory": "Không thể xóa thư mục nhiệm vụ: {{error}}", "custom_storage_path_unusable": "Đường dẫn lưu trữ tùy chỉnh \"{{path}}\" không thể sử dụng được, sẽ sử dụng đường dẫn mặc định", - "cannot_access_path": "Không thể truy cập đường dẫn {{path}}: {{error}}" + "cannot_access_path": "Không thể truy cập đường dẫn {{path}}: {{error}}", + "synthesization_failed": "Không thể tổng hợp ngữ cảnh cuộc trò chuyện." }, "warnings": { "no_terminal_content": "Không có nội dung terminal được chọn", @@ -67,7 +68,9 @@ "mcp_server_not_found": "Không tìm thấy máy chủ \"{{serverName}}\" trong cấu hình", "custom_storage_path_set": "Đã thiết lập đường dẫn lưu trữ tùy chỉnh: {{path}}", "default_storage_path": "Đã quay lại sử dụng đường dẫn lưu trữ mặc định", - "settings_imported": "Cài đặt đã được nhập thành công." + "settings_imported": "Cài đặt đã được nhập thành công.", + "synthesizing_context": "Đang tổng hợp ngữ cảnh cuộc trò chuyện...", + "synthesization_complete": "Đã hoàn tất tổng hợp ngữ cảnh cuộc trò chuyện." }, "answers": { "yes": "Có", diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 2fc49c9b378..dcd2cdfcea2 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -51,7 +51,8 @@ "failed_delete_repo": "删除关联的影子仓库或分支失败:{{error}}", "failed_remove_directory": "删除任务目录失败:{{error}}", "custom_storage_path_unusable": "自定义存储路径 \"{{path}}\" 不可用,将使用默认路径", - "cannot_access_path": "无法访问路径 {{path}}:{{error}}" + "cannot_access_path": "无法访问路径 {{path}}:{{error}}", + "synthesization_failed": "合成对话上下文失败。" }, "warnings": { "no_terminal_content": "没有选择终端内容", @@ -67,7 +68,9 @@ "mcp_server_not_found": "在配置中未找到服务器\"{{serverName}}\"", "custom_storage_path_set": "自定义存储路径已设置:{{path}}", "default_storage_path": "已恢复使用默认存储路径", - "settings_imported": "设置已成功导入。" + "settings_imported": "设置已成功导入。", + "synthesizing_context": "正在合成对话上下文...", + "synthesization_complete": "对话上下文合成完成。" }, "answers": { "yes": "是", diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index a51cfa0e9a4..5104ff5efe9 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -51,7 +51,8 @@ "failed_delete_repo": "刪除關聯的影子倉庫或分支失敗:{{error}}", "failed_remove_directory": "刪除工作目錄失敗:{{error}}", "custom_storage_path_unusable": "自訂儲存路徑 \"{{path}}\" 無法使用,將使用預設路徑", - "cannot_access_path": "無法存取路徑 {{path}}:{{error}}" + "cannot_access_path": "無法存取路徑 {{path}}:{{error}}", + "synthesization_failed": "合成對話上下文失敗。" }, "warnings": { "no_terminal_content": "沒有選擇終端機內容", @@ -67,7 +68,9 @@ "mcp_server_not_found": "在設定中沒有找到伺服器\"{{serverName}}\"", "custom_storage_path_set": "自訂儲存路徑已設定:{{path}}", "default_storage_path": "已恢復使用預設儲存路徑", - "settings_imported": "設定已成功匯入。" + "settings_imported": "設定已成功匯入。", + "synthesizing_context": "正在合成對話上下文...", + "synthesization_complete": "對話上下文合成完成。" }, "answers": { "yes": "是", diff --git a/src/schemas/index.ts b/src/schemas/index.ts index a3ec3381313..2bb084a549c 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -558,6 +558,12 @@ export const globalSettingsSchema = z.object({ terminalZdotdir: z.boolean().optional(), terminalCompressProgressBar: z.boolean().optional(), + // Context Synthesization Settings + enableContextSummarization: z.boolean().optional(), + contextSummarizationTriggerThreshold: z.number().optional(), // Percentage (e.g., 80) + contextSummarizationInitialStaticTurns: z.number().optional(), // Number of initial turns to keep + contextSummarizationRecentTurns: z.number().optional(), // Number of recent turns to keep + rateLimitSeconds: z.number().optional(), diffEnabled: z.boolean().optional(), fuzzyMatchThreshold: z.number().optional(), @@ -636,6 +642,12 @@ const globalSettingsRecord: GlobalSettingsRecord = { terminalZdotdir: undefined, terminalCompressProgressBar: undefined, + // Context Synthesization Settings + enableContextSummarization: undefined, + contextSummarizationTriggerThreshold: undefined, + contextSummarizationInitialStaticTurns: undefined, + contextSummarizationRecentTurns: undefined, + rateLimitSeconds: undefined, diffEnabled: undefined, fuzzyMatchThreshold: undefined, @@ -773,6 +785,7 @@ export const clineSays = [ "checkpoint_saved", "rooignore_error", "diff_error", + "summarizing", ] as const export const clineSaySchema = z.enum(clineSays) diff --git a/src/services/synthesization/ContextSynthesizer.ts b/src/services/synthesization/ContextSynthesizer.ts new file mode 100644 index 00000000000..97ba0e80829 --- /dev/null +++ b/src/services/synthesization/ContextSynthesizer.ts @@ -0,0 +1,148 @@ +import { Anthropic } from "@anthropic-ai/sdk" + +import { ApiHandler } from "../../api" + +/** + * Service responsible for synthesizing conversation history segments. + */ +export class ContextSynthesizer { + private apiHandler: ApiHandler + + constructor(apiHandler: ApiHandler) { + this.apiHandler = apiHandler + // TODO: Consider if a specific, potentially faster/cheaper model should be configured for synthesizing, + // possibly by accepting a separate ApiConfiguration or model ID in the constructor. + } + + /** + * Synthesizes a given array of conversation messages using an LLM. + * @param messagesToSynthesize The array of messages to be synthesized. + * @returns A promise that resolves to a new message object containing the synthesis, + * or null if synthesizing fails or is not possible. + */ + async synthesize(messagesToSynthesize: Anthropic.MessageParam[]): Promise { + if (messagesToSynthesize.length === 0) { + return null // Nothing to synthesize + } + + // Construct the prompt for the synthesizing model (User Final Refinement) + const systemPrompt = `You are a specialized context compression system for Roo-Code, a VS Code extension that enables AI coding agents. Your sole purpose is to condense conversation history while preserving maximum technical context with minimum tokens. + +**Context Schema:** +- You are synthesizing the MIDDLE portion of a conversation +- The original system prompt and initial interactions remain intact before your synthesis +- Recent conversation turns remain intact after your synthesis +- Your synthesis will be the critical bridge connecting these preserved segments + +**Content Priorities (Highest to Lowest):** +1. **Code Context:** + - Repository structure and file relationships + - Code snippets with their functionality and modifications + - Bugs/errors encountered and their solutions + - API endpoints and data structures + - Implementation decisions and their rationales + +2. **Tool Usage:** + - Tools that were invoked and their outputs + - File operations performed (creation, reading, modification) + - Files examined or referenced + - Terminal commands executed + - External APIs or services utilized + +3. **Task Progress:** + - Original user requirements and specifications + - Current implementation status + - Remaining tasks or issues + - Alternative approaches discussed and decisions made + - User feedback on implementations + +4. **Technical Information:** + - Language/framework specifics + - Environment configuration details + - Performance considerations + - Security requirements + - Testing approaches + +**Output Requirements:** +- Produce ONLY the synthesis text with no meta-commentary +- Use precise, technical language optimized for information density +- Structure with minimal formatting (use ## for major sections if necessary) +- Omit pleasantries, acknowledgments, and conversational elements +- Format sequences of related facts as compact, semicolon-separated phrases +- Use minimal tokens while maximizing preserved information +- Prioritize factual over instructional content + +This synthesis must enable seamless conversation continuity with no perceived context loss between the earlier and later preserved segments.` + + // Format the messages for the prompt. Simple stringification might be too verbose or lose structure. + // Let's try a more readable format. + const formattedMessages = messagesToSynthesize + .map((msg) => { + let contentText = "" + if (Array.isArray(msg.content)) { + contentText = msg.content + .map((block) => { + if (block.type === "text") return block.text + if (block.type === "image") return "[Image Content]" // Represent images concisely + // Add handling for other potential block types if necessary + return `[Unsupported Content: ${block.type}]` + }) + .join("\n") + } else { + contentText = msg.content + } + return `${msg.role.toUpperCase()}:\n${contentText}` + }) + .join("\n\n---\n\n") + + const userPrompt = `Please synthesize the following conversation turns:\n\n${formattedMessages}` + + try { + // Use the configured API handler to make the synthesizing call + // Note: This uses the main configured model. Consider allowing a specific synthesizing model. + // Disable prompt caching for synthesizing calls? - Currently not directly supported per-call. + // It will use the handler's configured caching setting. + const stream = this.apiHandler.createMessage( + systemPrompt, + [{ role: "user", content: userPrompt }], + undefined, // No specific cache key for synthesizing + // { promptCachingEnabled: false } // Removed incorrect 4th argument + ) + + let synthesisText = "" + let finalUsage = null + + // Consume the stream to get the full response + for await (const chunk of stream) { + if (chunk.type === "text") { + synthesisText += chunk.text + } else if (chunk.type === "usage") { + // Capture usage details if needed for cost tracking/logging + finalUsage = chunk + } + } + + if (finalUsage) { + // Optional: Log synthesizing cost/tokens + console.log( + `[Synthesizing] Usage: In=${finalUsage.inputTokens}, Out=${finalUsage.outputTokens}, Cost=${finalUsage.totalCost?.toFixed(6) ?? "N/A"}`, + ) + } + + if (!synthesisText || synthesisText.trim() === "") { + console.warn("Context synthesizing resulted in an empty synthesis.") + return null + } + + // Return the synthesis as a user message, representing the synthesized history. + return { + role: "user", // Represents the synthesized user/assistant interaction leading up to the current point. + content: `[Synthesized Conversation History]\n${synthesisText.trim()}`, + } + } catch (error) { + console.error("Context synthesizing API call failed:", error) + // TODO: Add more robust error handling/logging (e.g., telemetry) + return null // Indicate failure + } + } +} diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index bc4fbb66cb1..4f63fc66278 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -67,6 +67,7 @@ export interface ExtensionMessage { | "toggleApiConfigPin" | "acceptInput" | "setHistoryPreviewCollapsed" + | "synthesizationStatus" text?: string action?: | "chatButtonClicked" @@ -103,6 +104,7 @@ export interface ExtensionMessage { promptText?: string results?: { path: string; type: "file" | "folder"; label?: string }[] error?: string + status?: "started" | "completed" | "failed" } export type ExtensionState = Pick< @@ -164,6 +166,11 @@ export type ExtensionState = Pick< | "customModePrompts" | "customSupportPrompts" | "enhancementApiConfigId" + // Context Synthesization Settings (Added) + | "enableContextSummarization" + | "contextSummarizationTriggerThreshold" + | "contextSummarizationInitialStaticTurns" + | "contextSummarizationRecentTurns" > & { version: string clineMessages: ClineMessage[] @@ -200,6 +207,12 @@ export type ExtensionState = Pick< renderContext: "sidebar" | "editor" settingsImportedAt?: number historyPreviewCollapsed?: boolean + + // Context Synthesization Settings (Required part) + enableContextSummarization: boolean + contextSummarizationTriggerThreshold: number + contextSummarizationInitialStaticTurns: number + contextSummarizationRecentTurns: number } export type { ClineMessage, ClineAsk, ClineSay } diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index ad922f2c041..f2f01d5bb0c 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -127,6 +127,13 @@ export interface WebviewMessage { | "searchFiles" | "toggleApiConfigPin" | "setHistoryPreviewCollapsed" + // Context Synthesizing Settings + | "enableContextSummarization" + | "contextSummarizationTriggerThreshold" + | "contextSummarizationInitialStaticTurns" + | "contextSummarizationRecentTurns" + | "manualSynthesize" + | "synthesizationStatus" text?: string disabled?: boolean askResponse?: ClineAskResponse @@ -155,6 +162,7 @@ export interface WebviewMessage { hasSystemPromptOverride?: boolean terminalOperation?: "continue" | "abort" historyPreviewCollapsed?: boolean + status?: "started" | "completed" | "failed" } export const checkoutDiffPayloadSchema = z.object({ diff --git a/src/shared/context-mentions.ts b/src/shared/context-mentions.ts index 915114ab932..08c8d36db13 100644 --- a/src/shared/context-mentions.ts +++ b/src/shared/context-mentions.ts @@ -50,11 +50,11 @@ Mention regex: */ export const mentionRegex = - /@((?:\/|\w+:\/\/)[^\s]+?|[a-f0-9]{7,40}\b|problems\b|git-changes\b|terminal\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/ + /@((?:\/|\w+:\/\/)[^\s]+?|[a-f0-9]{7,40}\b|problems\b|git-changes\b|terminal\b|summarize\b)(?=[.,;:!?]?(?=[\s\r\n]|$))/ export const mentionRegexGlobal = new RegExp(mentionRegex.source, "g") export interface MentionSuggestion { - type: "file" | "folder" | "git" | "problems" + type: "file" | "folder" | "git" | "problems" | "summarize" label: string description?: string value: string diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 2c60d5f1aec..fc1304de1c9 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -252,6 +252,14 @@ const ChatTextArea = forwardRef( setShowContextMenu(false) setSelectedType(null) + if (type === ContextMenuOptionType.Synthesize) { + // Handle synthesize action - trigger manual synthesizing + vscode.postMessage({ type: "manualSynthesize" }) + setShowContextMenu(false) + setSelectedType(null) + return + } + if (textAreaRef.current) { let insertValue = value || "" @@ -267,6 +275,8 @@ const ChatTextArea = forwardRef( insertValue = value || "" } + // Note: The Summarize case is handled above before this block + const { newValue, mentionIndex } = insertMention( textAreaRef.current.value, cursorPosition, diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 20ffc73c3f0..74f56a62e5e 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -38,6 +38,7 @@ import ChatTextArea from "./ChatTextArea" import TaskHeader from "./TaskHeader" import AutoApproveMenu from "./AutoApproveMenu" import SystemPromptWarning from "./SystemPromptWarning" +import SynthesizingIndicator from "./SynthesizingIndicator" import { useTaskSearch } from "../history/useTaskSearch" export interface ChatViewProps { @@ -1247,6 +1248,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction )} + + {/* Synthesizing status indicator */} +
+ +
) : (
diff --git a/webview-ui/src/components/chat/ContextMenu.tsx b/webview-ui/src/components/chat/ContextMenu.tsx index 27d53803a85..62b90e91573 100644 --- a/webview-ui/src/components/chat/ContextMenu.tsx +++ b/webview-ui/src/components/chat/ContextMenu.tsx @@ -89,6 +89,8 @@ const ContextMenu: React.FC = ({ return Problems case ContextMenuOptionType.Terminal: return Terminal + case ContextMenuOptionType.Synthesize: + return {option.label || "Synthesize"} case ContextMenuOptionType.URL: return Paste URL to fetch contents case ContextMenuOptionType.NoResults: @@ -175,6 +177,8 @@ const ContextMenu: React.FC = ({ return "link" case ContextMenuOptionType.Git: return "git-commit" + case ContextMenuOptionType.Synthesize: + return "archive" case ContextMenuOptionType.NoResults: return "info" default: diff --git a/webview-ui/src/components/chat/SynthesizingIndicator.tsx b/webview-ui/src/components/chat/SynthesizingIndicator.tsx new file mode 100644 index 00000000000..5573ffba7e9 --- /dev/null +++ b/webview-ui/src/components/chat/SynthesizingIndicator.tsx @@ -0,0 +1,35 @@ +import React from "react" +import { cn } from "@/lib/utils" +import { useExtensionState } from "@/context/ExtensionStateContext" + +/** + * A component that displays the current status of context synthesizing + */ +export const SynthesizingIndicator: React.FC = () => { + const { synthesizationStatus } = useExtensionState() + + if (!synthesizationStatus) { + return null + } + + return ( +
+ {synthesizationStatus.status === "started" && ( + + )} + {synthesizationStatus.status === "completed" && } + {synthesizationStatus.status === "failed" && } + {synthesizationStatus.text} +
+ ) +} + +export default SynthesizingIndicator diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index 0f24316a12e..fe94e3be750 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -15,8 +15,22 @@ type ContextManagementSettingsProps = HTMLAttributes & { maxWorkspaceFiles: number showRooIgnoredFiles?: boolean maxReadFileLine?: number + // Context Synthesization Props (Added) + enableContextSummarization?: boolean + contextSummarizationTriggerThreshold?: number + contextSummarizationInitialStaticTurns?: number + contextSummarizationRecentTurns?: number setCachedStateField: SetCachedStateField< - "maxOpenTabsContext" | "maxWorkspaceFiles" | "showRooIgnoredFiles" | "maxReadFileLine" + // Update type to include new keys + | "maxOpenTabsContext" + | "maxWorkspaceFiles" + | "showRooIgnoredFiles" + | "maxReadFileLine" + // Context Synthesization Keys (Added) + | "enableContextSummarization" + | "contextSummarizationTriggerThreshold" + | "contextSummarizationInitialStaticTurns" + | "contextSummarizationRecentTurns" > } @@ -26,6 +40,11 @@ export const ContextManagementSettings = ({ showRooIgnoredFiles, setCachedStateField, maxReadFileLine, + // Context Synthesization Props (Added) + enableContextSummarization, + contextSummarizationTriggerThreshold, + contextSummarizationInitialStaticTurns, + contextSummarizationRecentTurns, className, ...props }: ContextManagementSettingsProps) => { @@ -127,6 +146,115 @@ export const ContextManagementSettings = ({ {t("settings:contextManagement.maxReadFile.description")}
+ + {/* --- Context Synthesization Settings --- */} +
+ +
+ setCachedStateField("enableContextSummarization", e.target.checked)} // Use generic setter + data-testid="enable-context-synthesization-checkbox"> + + +
+ {t("settings:contextManagement.synthesization.enable.description")} +
+
+ +
+
+ + {t("settings:contextManagement.synthesization.triggerThreshold.label")} + +
+ { + const newValue = parseInt(e.target.value, 10) + if (!isNaN(newValue) && newValue >= 1 && newValue <= 100) { + setCachedStateField("contextSummarizationTriggerThreshold", newValue) // Use generic setter + } + }} + onClick={(e) => e.currentTarget.select()} + data-testid="context-synthesization-trigger-threshold-input" + disabled={!enableContextSummarization} + /> + % +
+
+
+ {t("settings:contextManagement.synthesization.triggerThreshold.description")} +
+
+ +
+
+ + {t("settings:contextManagement.synthesization.initialTurns.label")} + +
+ { + const newValue = parseInt(e.target.value, 10) + if (!isNaN(newValue) && newValue >= 0) { + setCachedStateField("contextSummarizationInitialStaticTurns", newValue) // Use generic setter + } + }} + onClick={(e) => e.currentTarget.select()} + data-testid="context-synthesization-initial-turns-input" + disabled={!enableContextSummarization} + /> + {t("settings:contextManagement.synthesization.turns")} +
+
+
+ {t("settings:contextManagement.synthesization.initialTurns.description")} +
+
+ +
+
+ + {t("settings:contextManagement.synthesization.recentTurns.label")} + +
+ { + const newValue = parseInt(e.target.value, 10) + if (!isNaN(newValue) && newValue >= 0) { + setCachedStateField("contextSummarizationRecentTurns", newValue) // Use generic setter + } + }} + onClick={(e) => e.currentTarget.select()} + data-testid="context-synthesization-recent-turns-input" + disabled={!enableContextSummarization} + /> + {t("settings:contextManagement.synthesization.turns")} +
+
+
+ {t("settings:contextManagement.synthesization.recentTurns.description")} +
+
+ {/* --- End Context Synthesization Settings --- */} ) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index e12c9161597..1925f7483f3 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -259,6 +259,21 @@ const SettingsView = forwardRef(({ onDone, t vscode.postMessage({ type: "updateExperimental", values: experiments }) vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch }) vscode.postMessage({ type: "alwaysAllowSubtasks", bool: alwaysAllowSubtasks }) + // Context Synthesization Settings (Added - Use cachedState values) + vscode.postMessage({ type: "enableContextSummarization", bool: cachedState.enableContextSummarization }) + vscode.postMessage({ + type: "contextSummarizationTriggerThreshold", + value: cachedState.contextSummarizationTriggerThreshold, + }) + vscode.postMessage({ + type: "contextSummarizationInitialStaticTurns", + value: cachedState.contextSummarizationInitialStaticTurns, + }) + vscode.postMessage({ + type: "contextSummarizationRecentTurns", + value: cachedState.contextSummarizationRecentTurns, + }) + // --- End Context Synthesization --- vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting }) setChangeDetected(false) @@ -481,6 +496,12 @@ const SettingsView = forwardRef(({ onDone, t showRooIgnoredFiles={showRooIgnoredFiles} maxReadFileLine={maxReadFileLine} setCachedStateField={setCachedStateField} + // Pass synthesization state from cachedState (Added) + enableContextSummarization={cachedState.enableContextSummarization} + contextSummarizationTriggerThreshold={cachedState.contextSummarizationTriggerThreshold} + contextSummarizationInitialStaticTurns={cachedState.contextSummarizationInitialStaticTurns} + contextSummarizationRecentTurns={cachedState.contextSummarizationRecentTurns} + // Removed specific setters - ContextManagementSettings uses setCachedStateField now /> diff --git a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.test.tsx b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.test.tsx index 955ce619369..0f28a94698a 100644 --- a/webview-ui/src/components/settings/__tests__/ContextManagementSettings.test.tsx +++ b/webview-ui/src/components/settings/__tests__/ContextManagementSettings.test.tsx @@ -30,6 +30,16 @@ describe("ContextManagementSettings", () => { maxWorkspaceFiles: 200, showRooIgnoredFiles: false, setCachedStateField: jest.fn(), + // Add mock setters for new props (Added) + setEnableContextSummarization: jest.fn(), + setContextSummarizationTriggerThreshold: jest.fn(), + setContextSummarizationInitialStaticTurns: jest.fn(), + setContextSummarizationRecentTurns: jest.fn(), + // Add default values for new state props (Added) + enableContextSummarization: false, + contextSummarizationTriggerThreshold: 80, + contextSummarizationInitialStaticTurns: 5, + contextSummarizationRecentTurns: 10, } beforeEach(() => { @@ -51,6 +61,17 @@ describe("ContextManagementSettings", () => { const showRooIgnoredFilesCheckbox = screen.getByTestId("show-rooignored-files-checkbox") expect(showRooIgnoredFilesCheckbox).toBeInTheDocument() expect(screen.getByTestId("show-rooignored-files-checkbox")).not.toBeChecked() + + // Synthesization controls (Added) + expect(screen.getByTestId("enable-context-synthesization-checkbox")).toBeInTheDocument() + expect(screen.getByTestId("context-synthesization-trigger-threshold-input")).toBeInTheDocument() + expect(screen.getByTestId("context-synthesization-initial-turns-input")).toBeInTheDocument() + expect(screen.getByTestId("context-synthesization-recent-turns-input")).toBeInTheDocument() + + // Check initial disabled state for sub-settings (Added) + expect(screen.getByTestId("context-synthesization-trigger-threshold-input")).toBeDisabled() + expect(screen.getByTestId("context-synthesization-initial-turns-input")).toBeDisabled() + expect(screen.getByTestId("context-synthesization-recent-turns-input")).toBeDisabled() }) it("updates open tabs context limit", () => { @@ -79,4 +100,42 @@ describe("ContextManagementSettings", () => { expect(defaultProps.setCachedStateField).toHaveBeenCalledWith("showRooIgnoredFiles", true) }) + + // --- Tests for new synthesization settings --- (Added) + + it("enables sub-settings when synthesization is enabled", () => { + render() + + expect(screen.getByTestId("context-synthesization-trigger-threshold-input")).not.toBeDisabled() + expect(screen.getByTestId("context-synthesization-initial-turns-input")).not.toBeDisabled() + expect(screen.getByTestId("context-synthesization-recent-turns-input")).not.toBeDisabled() + }) + + it("updates enable context synthesization setting", () => { + render() + const checkbox = screen.getByTestId("enable-context-synthesization-checkbox") + fireEvent.click(checkbox) + expect(defaultProps.setCachedStateField).toHaveBeenCalledWith("enableContextSummarization", true) + }) + + it("updates synthesization trigger threshold", () => { + render() // Enable first + const input = screen.getByTestId("context-synthesization-trigger-threshold-input") + fireEvent.change(input, { target: { value: "95" } }) + expect(defaultProps.setCachedStateField).toHaveBeenCalledWith("contextSummarizationTriggerThreshold", 95) + }) + + it("updates initial turns to keep", () => { + render() // Enable first + const input = screen.getByTestId("context-synthesization-initial-turns-input") + fireEvent.change(input, { target: { value: "3" } }) + expect(defaultProps.setCachedStateField).toHaveBeenCalledWith("contextSummarizationInitialStaticTurns", 3) + }) + + it("updates recent turns to keep", () => { + render() // Enable first + const input = screen.getByTestId("context-synthesization-recent-turns-input") + fireEvent.change(input, { target: { value: "12" } }) + expect(defaultProps.setCachedStateField).toHaveBeenCalledWith("contextSummarizationRecentTurns", 12) + }) }) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 3a2cde62823..fd074af2cb9 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -20,6 +20,10 @@ export interface ExtensionStateContextType extends ExtensionState { mcpServers: McpServer[] hasSystemPromptOverride?: boolean currentCheckpoint?: string + synthesizationStatus?: { + status: "started" | "completed" | "failed" + text: string + } filePaths: string[] openedTabs: Array<{ label: string; isActive: boolean; path?: string }> setApiConfiguration: (config: ApiConfiguration) => void @@ -93,6 +97,16 @@ export interface ExtensionStateContextType extends ExtensionState { terminalCompressProgressBar?: boolean setTerminalCompressProgressBar: (value: boolean) => void setHistoryPreviewCollapsed: (value: boolean) => void + + // Context Synthesization Setters + enableContextSummarization: boolean + setEnableContextSummarization: (value: boolean) => void + contextSummarizationTriggerThreshold: number + setContextSummarizationTriggerThreshold: (value: number) => void + contextSummarizationInitialStaticTurns: number + setContextSummarizationInitialStaticTurns: (value: number) => void + contextSummarizationRecentTurns: number + setContextSummarizationRecentTurns: (value: number) => void } export const ExtensionStateContext = createContext(undefined) @@ -171,6 +185,12 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode terminalZdotdir: false, // Default ZDOTDIR handling setting terminalCompressProgressBar: true, // Default to compress progress bar output historyPreviewCollapsed: false, // Initialize the new state (default to expanded) + + // Context Synthesization Defaults (Added) + enableContextSummarization: false, + contextSummarizationTriggerThreshold: 80, + contextSummarizationInitialStaticTurns: 5, + contextSummarizationRecentTurns: 10, }) const [didHydrateState, setDidHydrateState] = useState(false) @@ -232,6 +252,41 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setCurrentCheckpoint(message.text) break } + case "synthesizationStatus": { + // For "started" status, update the state immediately + if (message.status === "started") { + setState((prevState) => ({ + ...prevState, + synthesizationStatus: { + status: message.status as "started" | "completed" | "failed", + text: message.text || "", + }, + })) + } + // For "completed" or "failed" status, update and then clear after delay + else if (message.status === "completed" || message.status === "failed") { + // First update the status + setState((prevState) => ({ + ...prevState, + synthesizationStatus: { + status: message.status as "completed" | "failed", + text: message.text || "", + }, + })) + + // Then clear it after a delay + const timer = setTimeout(() => { + setState((prevState) => ({ + ...prevState, + synthesizationStatus: undefined, + })) + }, 3000) // Reduced to 3 seconds for better UX + + // Clean up the timer if component unmounts + return () => clearTimeout(timer) + } + break + } case "listApiConfig": { setListApiConfigMeta(message.listApiConfig ?? []) break @@ -345,6 +400,24 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }), setHistoryPreviewCollapsed: (value) => setState((prevState) => ({ ...prevState, historyPreviewCollapsed: value })), // Implement the setter + + // Context Synthesization Setters Implementation + setEnableContextSummarization: (value) => { + setState((prevState) => ({ ...prevState, enableContextSummarization: value })) + vscode.postMessage({ type: "enableContextSummarization", bool: value }) + }, + setContextSummarizationTriggerThreshold: (value) => { + setState((prevState) => ({ ...prevState, contextSummarizationTriggerThreshold: value })) + vscode.postMessage({ type: "contextSummarizationTriggerThreshold", value }) + }, + setContextSummarizationInitialStaticTurns: (value) => { + setState((prevState) => ({ ...prevState, contextSummarizationInitialStaticTurns: value })) + vscode.postMessage({ type: "contextSummarizationInitialStaticTurns", value }) + }, + setContextSummarizationRecentTurns: (value) => { + setState((prevState) => ({ ...prevState, contextSummarizationRecentTurns: value })) + vscode.postMessage({ type: "contextSummarizationRecentTurns", value }) + }, } return {children} diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx index 1ba2d87e9a4..d164a69221c 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx @@ -202,6 +202,11 @@ describe("mergeExtensionState", () => { showRooIgnoredFiles: true, renderContext: "sidebar", maxReadFileLine: 500, + // Context Synthesization Defaults (Added for test) + enableContextSummarization: false, + contextSummarizationTriggerThreshold: 80, + contextSummarizationInitialStaticTurns: 5, + contextSummarizationRecentTurns: 10, } const prevState: ExtensionState = { diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index a3896118644..77318fd5d4d 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -301,6 +301,25 @@ "description": "Roo llegeix aquest nombre de línies quan el model omet els valors d'inici/final. Si aquest nombre és menor que el total del fitxer, Roo genera un índex de números de línia de les definicions de codi. Casos especials: -1 indica a Roo que llegeixi tot el fitxer (sense indexació), i 0 indica que no llegeixi cap línia i proporcioni només índexs de línia per a un context mínim. Valors més baixos minimitzen l'ús inicial de context, permetent lectures posteriors de rangs de línies precisos. Les sol·licituds amb inici/final explícits no estan limitades per aquesta configuració.", "lines": "línies", "always_full_read": "Llegeix sempre el fitxer sencer" + }, + "synthesization": { + "enable": { + "label": "Activar resum de context", + "description": "Quan està activat, els torns de conversa més antics es resumiran en lloc de truncar-se quan s'acosti el límit de context. Això conserva més informació però implica costos addicionals de token per al resum." + }, + "triggerThreshold": { + "label": "Llindar d'activació del resum", + "description": "Percentatge de la mida de la finestra de context en què s'ha d'activar el resum (p. ex., 80%)." + }, + "initialTurns": { + "label": "Torns inicials a mantenir", + "description": "Nombre de missatges inicials de conversa (prompt del sistema + usuari/assistent) a mantenir sempre amb detall complet." + }, + "recentTurns": { + "label": "Torns recents a mantenir", + "description": "Nombre dels missatges de conversa més recents a mantenir sempre amb detall complet." + }, + "turns": "missatges" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index e0f470e8e07..e0d17d94252 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -301,6 +301,25 @@ "description": "Roo liest diese Anzahl von Zeilen, wenn das Modell keine Start-/Endwerte angibt. Wenn diese Zahl kleiner als die Gesamtzahl der Zeilen ist, erstellt Roo einen Zeilennummernindex der Codedefinitionen. Spezialfälle: -1 weist Roo an, die gesamte Datei zu lesen (ohne Indexierung), und 0 weist an, keine Zeilen zu lesen und nur Zeilenindizes für minimalen Kontext bereitzustellen. Niedrigere Werte minimieren die anfängliche Kontextnutzung und ermöglichen präzise nachfolgende Zeilenbereich-Lesungen. Explizite Start-/End-Anfragen sind von dieser Einstellung nicht begrenzt.", "lines": "Zeilen", "always_full_read": "Immer die gesamte Datei lesen" + }, + "synthesization": { + "enable": { + "label": "Kontextzusammenfassung aktivieren", + "description": "Wenn aktiviert, werden ältere Gesprächsrunden zusammengefasst statt abgeschnitten, wenn das Kontextlimit erreicht wird. Dies bewahrt mehr Informationen, verursacht aber zusätzliche Token-Kosten für die Zusammenfassung." + }, + "triggerThreshold": { + "label": "Auslöseschwelle für Zusammenfassung", + "description": "Prozentsatz der Kontextfenstergröße, bei dem die Zusammenfassung ausgelöst werden soll (z. B. 80%)." + }, + "initialTurns": { + "label": "Anfängliche Runden beibehalten", + "description": "Anzahl der anfänglichen Gesprächsnachrichten (System-Prompt + Benutzer/Assistent), die immer vollständig detailliert beibehalten werden sollen." + }, + "recentTurns": { + "label": "Letzte Runden beibehalten", + "description": "Anzahl der letzten Gesprächsnachrichten, die immer vollständig detailliert beibehalten werden sollen." + }, + "turns": "Nachrichten" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index b057d94043c..eca71449890 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -301,6 +301,25 @@ "description": "Roo reads this number of lines when the model omits start/end values. If this number is less than the file's total, Roo generates a line number index of code definitions. Special cases: -1 instructs Roo to read the entire file (without indexing), and 0 instructs it to read no lines and provides line indexes only for minimal context. Lower values minimize initial context usage, enabling precise subsequent line-range reads. Explicit start/end requests are not limited by this setting.", "lines": "lines", "always_full_read": "Always read entire file" + }, + "synthesization": { + "enable": { + "label": "Enable automatic context synthesization", + "description": "When enabled, older conversation turns will be synthesized instead of truncated when the context limit is approached. This preserves more information but incurs additional token costs for synthesization." + }, + "triggerThreshold": { + "label": "Synthesization trigger threshold", + "description": "Percentage of the context window size at which synthesization should be triggered (e.g., 80%)." + }, + "initialTurns": { + "label": "Initial turns to keep", + "description": "Number of initial conversation messages (system prompt + user/assistant) to always keep in full detail." + }, + "recentTurns": { + "label": "Recent turns to keep", + "description": "Number of the most recent conversation messages to always keep in full detail." + }, + "turns": "messages" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 5b810ea168c..2498445dfed 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -301,6 +301,25 @@ "description": "Roo lee este número de líneas cuando el modelo omite valores de inicio/fin. Si este número es menor que el total del archivo, Roo genera un índice de números de línea de las definiciones de código. Casos especiales: -1 indica a Roo que lea el archivo completo (sin indexación), y 0 indica que no lea líneas y proporcione solo índices de línea para un contexto mínimo. Valores más bajos minimizan el uso inicial de contexto, permitiendo lecturas posteriores de rangos de líneas precisos. Las solicitudes con inicio/fin explícitos no están limitadas por esta configuración.", "lines": "líneas", "always_full_read": "Siempre leer el archivo completo" + }, + "synthesization": { + "enable": { + "label": "Habilitar resumen de contexto", + "description": "Cuando está habilitado, los turnos de conversación más antiguos se resumirán en lugar de truncarse cuando se acerque el límite de contexto. Esto conserva más información pero incurre en costos adicionales de token para el resumen." + }, + "triggerThreshold": { + "label": "Umbral de activación del resumen", + "description": "Porcentaje del tamaño de la ventana de contexto en el que se debe activar el resumen (p. ej., 80%)." + }, + "initialTurns": { + "label": "Turnos iniciales a mantener", + "description": "Número de mensajes iniciales de la conversación (prompt del sistema + usuario/asistente) que siempre se mantendrán con todo detalle." + }, + "recentTurns": { + "label": "Turnos recientes a mantener", + "description": "Número de los mensajes de conversación más recientes que siempre se mantendrán con todo detalle." + }, + "turns": "mensajes" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 25efb24c1fb..bdcbce329cf 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -301,6 +301,25 @@ "description": "Roo lit ce nombre de lignes lorsque le modèle omet les valeurs de début/fin. Si ce nombre est inférieur au total du fichier, Roo génère un index des numéros de ligne des définitions de code. Cas spéciaux : -1 indique à Roo de lire le fichier entier (sans indexation), et 0 indique de ne lire aucune ligne et de fournir uniquement les index de ligne pour un contexte minimal. Des valeurs plus basses minimisent l'utilisation initiale du contexte, permettant des lectures ultérieures de plages de lignes précises. Les requêtes avec début/fin explicites ne sont pas limitées par ce paramètre.", "lines": "lignes", "always_full_read": "Toujours lire le fichier entier" + }, + "synthesization": { + "enable": { + "label": "Activer le résumé du contexte", + "description": "Lorsque cette option est activée, les anciens tours de conversation seront résumés au lieu d'être tronqués à l'approche de la limite de contexte. Cela préserve plus d'informations mais entraîne des coûts de token supplémentaires pour le résumé." + }, + "triggerThreshold": { + "label": "Seuil de déclenchement du résumé", + "description": "Pourcentage de la taille de la fenêtre de contexte auquel le résumé doit être déclenché (par ex., 80%)." + }, + "initialTurns": { + "label": "Tours initiaux à conserver", + "description": "Nombre de messages de conversation initiaux (prompt système + utilisateur/assistant) à toujours conserver en détail." + }, + "recentTurns": { + "label": "Tours récents à conserver", + "description": "Nombre des messages de conversation les plus récents à toujours conserver en détail." + }, + "turns": "messages" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 5cc20642dac..648cd9f0013 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -301,6 +301,25 @@ "description": "जब मॉडल प्रारंभ/अंत मान नहीं देता है, तो Roo इतनी पंक्तियाँ पढ़ता है। यदि यह संख्या फ़ाइल की कुल पंक्तियों से कम है, तो Roo कोड परिभाषाओं का पंक्ति क्रमांक इंडेक्स बनाता है। विशेष मामले: -1 Roo को पूरी फ़ाइल पढ़ने का निर्देश देता है (इंडेक्सिंग के बिना), और 0 कोई पंक्ति न पढ़ने और न्यूनतम संदर्भ के लिए केवल पंक्ति इंडेक्स प्रदान करने का निर्देश देता है। कम मान प्रारंभिक संदर्भ उपयोग को कम करते हैं, जो बाद में सटीक पंक्ति श्रेणी पढ़ने की अनुमति देता है। स्पष्ट प्रारंभ/अंत अनुरोध इस सेटिंग से सीमित नहीं हैं।", "lines": "पंक्तियाँ", "always_full_read": "हमेशा पूरी फ़ाइल पढ़ें" + }, + "synthesization": { + "enable": { + "label": "संदर्भ सारांश सक्षम करें", + "description": "सक्षम होने पर, संदर्भ सीमा के करीब पहुंचने पर पुरानी बातचीत के दौरों को छोटा करने के बजाय सारांशित किया जाएगा। यह अधिक जानकारी सुरक्षित रखता है लेकिन सारांश के लिए अतिरिक्त टोकन लागत लगती है।" + }, + "triggerThreshold": { + "label": "सारांश ट्रिगर थ्रेशोल्ड", + "description": "संदर्भ विंडो आकार का प्रतिशत जिस पर सारांश ट्रिगर किया जाना चाहिए (उदाहरण के लिए, 80%)।" + }, + "initialTurns": { + "label": "शुरुआती दौर बनाए रखें", + "description": "शुरुआती बातचीत संदेशों की संख्या (सिस्टम प्रॉम्प्ट + उपयोगकर्ता/सहायक) जिन्हें हमेशा पूरे विवरण में रखा जाना चाहिए।" + }, + "recentTurns": { + "label": "हाल के दौर बनाए रखें", + "description": "सबसे हाल के बातचीत संदेशों की संख्या जिन्हें हमेशा पूरे विवरण में रखा जाना चाहिए।" + }, + "turns": "संदेश" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 55866e53504..7fd66c24b63 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -301,6 +301,25 @@ "description": "Roo legge questo numero di righe quando il modello omette i valori di inizio/fine. Se questo numero è inferiore al totale del file, Roo genera un indice dei numeri di riga delle definizioni di codice. Casi speciali: -1 indica a Roo di leggere l'intero file (senza indicizzazione), e 0 indica di non leggere righe e fornire solo indici di riga per un contesto minimo. Valori più bassi minimizzano l'utilizzo iniziale del contesto, permettendo successive letture precise di intervalli di righe. Le richieste con inizio/fine espliciti non sono limitate da questa impostazione.", "lines": "righe", "always_full_read": "Leggi sempre l'intero file" + }, + "synthesization": { + "enable": { + "label": "Abilita riassunto del contesto", + "description": "Se abilitato, i turni di conversazione più vecchi verranno riassunti invece di essere troncati all'avvicinarsi del limite di contesto. Ciò preserva più informazioni ma comporta costi aggiuntivi di token per il riassunto." + }, + "triggerThreshold": { + "label": "Soglia di attivazione del riassunto", + "description": "Percentuale della dimensione della finestra di contesto alla quale attivare il riassunto (es. 80%)." + }, + "initialTurns": { + "label": "Turni iniziali da mantenere", + "description": "Numero di messaggi di conversazione iniziali (prompt di sistema + utente/assistente) da mantenere sempre con tutti i dettagli." + }, + "recentTurns": { + "label": "Turni recenti da mantenere", + "description": "Numero dei messaggi di conversazione più recenti da mantenere sempre con tutti i dettagli." + }, + "turns": "messaggi" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index b7960b9b455..996f634a832 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -301,6 +301,25 @@ "description": "モデルが開始/終了の値を指定しない場合、Rooはこの行数を読み込みます。この数がファイルの総行数より少ない場合、Rooはコード定義の行番号インデックスを生成します。特殊なケース:-1はRooにファイル全体を読み込むよう指示し(インデックス作成なし)、0は行を読み込まず最小限のコンテキストのために行インデックスのみを提供するよう指示します。低い値は初期コンテキスト使用量を最小限に抑え、後続の正確な行範囲の読み込みを可能にします。明示的な開始/終了の要求はこの設定による制限を受けません。", "lines": "行", "always_full_read": "常にファイル全体を読み込む" + }, + "synthesization": { + "enable": { + "label": "コンテキスト要約を有効にする", + "description": "有効にすると、コンテキスト制限に近づいたときに古い会話のターンが切り捨てられる代わりに要約されます。これにより、より多くの情報が保持されますが、要約に追加のトークンコストが発生します。" + }, + "triggerThreshold": { + "label": "要約トリガーしきい値", + "description": "要約をトリガーするコンテキストウィンドウサイズの割合(例:80%)。" + }, + "initialTurns": { + "label": "保持する初期ターン", + "description": "常に詳細に保持する初期の会話メッセージ(システムプロンプト+ユーザー/アシスタント)の数。" + }, + "recentTurns": { + "label": "保持する最近のターン", + "description": "常に詳細に保持する最新の会話メッセージの数。" + }, + "turns": "メッセージ" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index b513a308cee..7ef2ec780fb 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -301,6 +301,25 @@ "description": "모델이 시작/끝 값을 지정하지 않을 때 Roo가 읽는 줄 수입니다. 이 수가 파일의 총 줄 수보다 적으면 Roo는 코드 정의의 줄 번호 인덱스를 생성합니다. 특수한 경우: -1은 Roo에게 전체 파일을 읽도록 지시하고(인덱싱 없이), 0은 줄을 읽지 않고 최소한의 컨텍스트를 위해 줄 인덱스만 제공하도록 지시합니다. 낮은 값은 초기 컨텍스트 사용을 최소화하고, 이후 정확한 줄 범위 읽기를 가능하게 합니다. 명시적 시작/끝 요청은 이 설정의 제한을 받지 않습니다.", "lines": "줄", "always_full_read": "항상 전체 파일 읽기" + }, + "synthesization": { + "enable": { + "label": "컨텍스트 요약 활성화", + "description": "활성화하면 컨텍스트 제한에 가까워질 때 이전 대화 턴이 잘리는 대신 요약됩니다. 이렇게 하면 더 많은 정보가 보존되지만 요약에 추가 토큰 비용이 발생합니다." + }, + "triggerThreshold": { + "label": "요약 트리거 임계값", + "description": "요약이 트리거되어야 하는 컨텍스트 창 크기의 백분율(예: 80%)." + }, + "initialTurns": { + "label": "유지할 초기 턴", + "description": "항상 전체 세부 정보로 유지할 초기 대화 메시지 수(시스템 프롬프트 + 사용자/어시스턴트)." + }, + "recentTurns": { + "label": "유지할 최근 턴", + "description": "항상 전체 세부 정보로 유지할 가장 최근 대화 메시지 수." + }, + "turns": "메시지" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 8f80043fa0a..adb58274dee 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -301,6 +301,25 @@ "description": "Roo odczytuje tę liczbę linii, gdy model nie określa wartości początkowej/końcowej. Jeśli ta liczba jest mniejsza niż całkowita liczba linii pliku, Roo generuje indeks numerów linii definicji kodu. Przypadki specjalne: -1 nakazuje Roo odczytać cały plik (bez indeksowania), a 0 nakazuje nie czytać żadnych linii i dostarczyć tylko indeksy linii dla minimalnego kontekstu. Niższe wartości minimalizują początkowe użycie kontekstu, umożliwiając późniejsze precyzyjne odczyty zakresów linii. Jawne żądania początku/końca nie są ograniczone tym ustawieniem.", "lines": "linii", "always_full_read": "Zawsze czytaj cały plik" + }, + "synthesization": { + "enable": { + "label": "Włącz podsumowanie kontekstu", + "description": "Gdy włączone, starsze tury konwersacji będą podsumowywane zamiast obcinane, gdy zbliża się limit kontekstu. Zachowuje to więcej informacji, ale wiąże się z dodatkowymi kosztami tokenów za podsumowanie." + }, + "triggerThreshold": { + "label": "Próg wyzwalania podsumowania", + "description": "Procent rozmiaru okna kontekstu, przy którym powinno zostać wyzwolone podsumowanie (np. 80%)." + }, + "initialTurns": { + "label": "Początkowe tury do zachowania", + "description": "Liczba początkowych wiadomości konwersacji (prompt systemowy + użytkownik/asystent), które zawsze mają być zachowane w pełnej szczegółowości." + }, + "recentTurns": { + "label": "Ostatnie tury do zachowania", + "description": "Liczba najnowszych wiadomości konwersacji, które zawsze mają być zachowane w pełnej szczegółowości." + }, + "turns": "wiadomości" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index d27a053247c..3578d268efe 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -301,6 +301,25 @@ "description": "O Roo lê este número de linhas quando o modelo omite valores de início/fim. Se este número for menor que o total do arquivo, o Roo gera um índice de números de linha das definições de código. Casos especiais: -1 instrui o Roo a ler o arquivo inteiro (sem indexação), e 0 instrui a não ler linhas e fornecer apenas índices de linha para contexto mínimo. Valores mais baixos minimizam o uso inicial de contexto, permitindo leituras posteriores precisas de intervalos de linhas. Requisições com início/fim explícitos não são limitadas por esta configuração.", "lines": "linhas", "always_full_read": "Sempre ler o arquivo inteiro" + }, + "synthesization": { + "enable": { + "label": "Habilitar resumo de contexto", + "description": "Quando habilitado, os turnos de conversa mais antigos serão resumidos em vez de truncados quando o limite de contexto for atingido. Isso preserva mais informações, mas incorre em custos adicionais de token para o resumo." + }, + "triggerThreshold": { + "label": "Limite de gatilho do resumo", + "description": "Percentual do tamanho da janela de contexto em que o resumo deve ser acionado (por exemplo, 80%)." + }, + "initialTurns": { + "label": "Turnos iniciais a manter", + "description": "Número de mensagens iniciais da conversa (prompt do sistema + usuário/assistente) a serem sempre mantidas em detalhes completos." + }, + "recentTurns": { + "label": "Turnos recentes a manter", + "description": "Número das mensagens de conversa mais recentes a serem sempre mantidas em detalhes completos." + }, + "turns": "mensagens" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 27f1240a89f..b6564b33f0a 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -301,6 +301,25 @@ "description": "Roo читает столько строк, если модель не указала явно начало/конец. Если число меньше общего количества строк в файле, Roo создаёт индекс определений кода по строкам. Особые случаи: -1 — Roo читает весь файл (без индексации), 0 — не читает строки, а создаёт только минимальный индекс. Меньшие значения минимизируют начальный контекст, позволяя точнее читать нужные диапазоны строк. Явные запросы начала/конца не ограничиваются этим параметром.", "lines": "строк", "always_full_read": "Всегда читать весь файл" + }, + "synthesization": { + "enable": { + "label": "Включить суммирование контекста", + "description": "Если включено, старые ходы беседы будут суммироваться, а не обрезаться при приближении к лимиту контекста. Это сохраняет больше информации, но влечет за собой дополнительные затраты токенов на суммирование." + }, + "triggerThreshold": { + "label": "Порог срабатывания суммирования", + "description": "Процент размера окна контекста, при котором должно срабатывать суммирование (например, 80%)." + }, + "initialTurns": { + "label": "Начальные ходы для сохранения", + "description": "Количество начальных сообщений беседы (системный промпт + пользователь/ассистент), которые всегда должны сохраняться в полной детализации." + }, + "recentTurns": { + "label": "Последние ходы для сохранения", + "description": "Количество самых последних сообщений беседы, которые всегда должны сохраняться в полной детализации." + }, + "turns": "сообщения" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 94861a6139c..2f90915a38d 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -301,6 +301,25 @@ "description": "Model başlangıç/bitiş değerlerini belirtmediğinde Roo bu sayıda satırı okur. Bu sayı dosyanın toplam satır sayısından azsa, Roo kod tanımlamalarının satır numarası dizinini oluşturur. Özel durumlar: -1, Roo'ya tüm dosyayı okumasını (dizinleme olmadan), 0 ise hiç satır okumamasını ve minimum bağlam için yalnızca satır dizinleri sağlamasını belirtir. Düşük değerler başlangıç bağlam kullanımını en aza indirir ve sonraki hassas satır aralığı okumalarına olanak tanır. Açık başlangıç/bitiş istekleri bu ayarla sınırlı değildir.", "lines": "satır", "always_full_read": "Her zaman tüm dosyayı oku" + }, + "synthesization": { + "enable": { + "label": "Bağlam özetlemeyi etkinleştir", + "description": "Etkinleştirildiğinde, bağlam sınırına yaklaşıldığında eski konuşma turları kesilmek yerine özetlenir. Bu, daha fazla bilgi korur ancak özetleme için ek token maliyetlerine neden olur." + }, + "triggerThreshold": { + "label": "Özetleme tetikleme eşiği", + "description": "Özetlemenin tetiklenmesi gereken bağlam penceresi boyutunun yüzdesi (ör. %80)." + }, + "initialTurns": { + "label": "Saklanacak ilk turlar", + "description": "Her zaman tam ayrıntılarıyla saklanacak ilk konuşma mesajlarının sayısı (sistem istemi + kullanıcı/asistan)." + }, + "recentTurns": { + "label": "Saklanacak son turlar", + "description": "Her zaman tam ayrıntılarıyla saklanacak en son konuşma mesajlarının sayısı." + }, + "turns": "mesajlar" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 87941b5b8e9..a3ddeb47ccf 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -301,6 +301,25 @@ "description": "Roo đọc số dòng này khi mô hình không chỉ định giá trị bắt đầu/kết thúc. Nếu số này nhỏ hơn tổng số dòng của tệp, Roo sẽ tạo một chỉ mục số dòng của các định nghĩa mã. Trường hợp đặc biệt: -1 chỉ thị Roo đọc toàn bộ tệp (không tạo chỉ mục), và 0 chỉ thị không đọc dòng nào và chỉ cung cấp chỉ mục dòng cho ngữ cảnh tối thiểu. Giá trị thấp hơn giảm thiểu việc sử dụng ngữ cảnh ban đầu, cho phép đọc chính xác các phạm vi dòng sau này. Các yêu cầu có chỉ định bắt đầu/kết thúc rõ ràng không bị giới hạn bởi cài đặt này.", "lines": "dòng", "always_full_read": "Luôn đọc toàn bộ tệp" + }, + "synthesization": { + "enable": { + "label": "Bật tóm tắt ngữ cảnh", + "description": "Khi được bật, các lượt trò chuyện cũ hơn sẽ được tóm tắt thay vì bị cắt bớt khi đạt đến giới hạn ngữ cảnh. Điều này bảo toàn nhiều thông tin hơn nhưng phải trả thêm chi phí token cho việc tóm tắt." + }, + "triggerThreshold": { + "label": "Ngưỡng kích hoạt tóm tắt", + "description": "Phần trăm kích thước cửa sổ ngữ cảnh mà tại đó việc tóm tắt sẽ được kích hoạt (ví dụ: 80%)." + }, + "initialTurns": { + "label": "Lượt ban đầu cần giữ lại", + "description": "Số lượng tin nhắn trò chuyện ban đầu (lời nhắc hệ thống + người dùng/trợ lý) luôn được giữ lại đầy đủ chi tiết." + }, + "recentTurns": { + "label": "Lượt gần đây cần giữ lại", + "description": "Số lượng tin nhắn trò chuyện gần đây nhất luôn được giữ lại đầy đủ chi tiết." + }, + "turns": "tin nhắn" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index fd10b77b63b..41bc755cab6 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -301,6 +301,25 @@ "description": "自动读取文件行数设置:-1=完整读取 0=仅生成行号索引,较小值可节省token,支持后续使用行号进行读取。", "lines": "行", "always_full_read": "始终读取整个文件" + }, + "synthesization": { + "enable": { + "label": "启用上下文摘要", + "description": "启用后,当接近上下文限制时,较早的对话轮次将被摘要而不是截断。这可以保留更多信息,但会产生额外的 token 费用用于摘要。" + }, + "triggerThreshold": { + "label": "摘要触发阈值", + "description": "应触发摘要的上下文窗口大小百分比(例如 80%)。" + }, + "initialTurns": { + "label": "保留的初始轮次", + "description": "始终保留完整细节的初始对话消息数量(系统提示 + 用户/助手)。" + }, + "recentTurns": { + "label": "保留的最近轮次", + "description": "始终保留完整细节的最近对话消息数量。" + }, + "turns": "条消息" } }, "terminal": { diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index a40be4d17fd..a3f523f8c04 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -301,6 +301,25 @@ "description": "當模型未指定起始/結束值時,Roo 讀取的行數。如果此數值小於檔案總行數,Roo 將產生程式碼定義的行號索引。特殊情況:-1 指示 Roo 讀取整個檔案(不建立索引),0 指示不讀取任何行並僅提供行索引以取得最小上下文。較低的值可最小化初始上下文使用,允許後續精確的行範圍讀取。明確指定起始/結束的請求不受此設定限制。", "lines": "行", "always_full_read": "始終讀取整個檔案" + }, + "synthesization": { + "enable": { + "label": "啟用內容摘要", + "description": "啟用後,當接近內容限制時,較早的對話輪次將被摘要而不是截斷。這可以保留更多資訊,但會產生額外的 token 費用用於摘要。" + }, + "triggerThreshold": { + "label": "摘要觸發閾值", + "description": "應觸發摘要的內容視窗大小百分比(例如 80%)。" + }, + "initialTurns": { + "label": "保留的初始輪次", + "description": "始終保留完整細節的初始對話訊息數量(系統提示 + 使用者/助理)。" + }, + "recentTurns": { + "label": "保留的最近輪次", + "description": "始終保留完整細節的最近對話訊息數量。" + }, + "turns": "則訊息" } }, "terminal": { diff --git a/webview-ui/src/utils/__tests__/context-mentions.test.ts b/webview-ui/src/utils/__tests__/context-mentions.test.ts index d28f2de6402..92a6c1e7138 100644 --- a/webview-ui/src/utils/__tests__/context-mentions.test.ts +++ b/webview-ui/src/utils/__tests__/context-mentions.test.ts @@ -91,8 +91,9 @@ describe("getContextMenuOptions", () => { it("should return all option types for empty query", () => { const result = getContextMenuOptions("", "", null, []) - expect(result).toHaveLength(6) + expect(result).toHaveLength(7) expect(result.map((item) => item.type)).toEqual([ + ContextMenuOptionType.Synthesize, ContextMenuOptionType.Problems, ContextMenuOptionType.Terminal, ContextMenuOptionType.URL, diff --git a/webview-ui/src/utils/context-mentions.ts b/webview-ui/src/utils/context-mentions.ts index 293aa8f74bb..83ee06cc3e9 100644 --- a/webview-ui/src/utils/context-mentions.ts +++ b/webview-ui/src/utils/context-mentions.ts @@ -1,7 +1,11 @@ import { mentionRegex } from "@roo/shared/context-mentions" import { Fzf } from "fzf" import { ModeConfig } from "@roo/shared/modes" -import * as path from "path" + +// Simple basename function to replace path.basename +function basename(filepath: string): string { + return filepath.split("/").pop() || filepath +} export interface SearchResult { path: string @@ -77,6 +81,7 @@ export enum ContextMenuOptionType { Git = "git", NoResults = "noResults", Mode = "mode", // Add mode type + Synthesize = "synthesize", // Add synthesize type } export interface ContextMenuQueryItem { @@ -164,6 +169,11 @@ export function getContextMenuOptions( } return [ + { + type: ContextMenuOptionType.Synthesize, + label: "Synthesize", + description: "Compress conversation history", + }, { type: ContextMenuOptionType.Problems }, { type: ContextMenuOptionType.Terminal }, { type: ContextMenuOptionType.URL }, @@ -193,6 +203,13 @@ export function getContextMenuOptions( if ("terminal".startsWith(lowerQuery)) { suggestions.push({ type: ContextMenuOptionType.Terminal }) } + if ("synthesize".startsWith(lowerQuery)) { + suggestions.push({ + type: ContextMenuOptionType.Synthesize, + label: "Synthesize", + description: "Compress conversation history", + }) + } if (query.startsWith("http")) { suggestions.push({ type: ContextMenuOptionType.URL, value: query }) } @@ -241,7 +258,7 @@ export function getContextMenuOptions( return { type: result.type === "folder" ? ContextMenuOptionType.Folder : ContextMenuOptionType.File, value: formattedPath, - label: result.label || path.basename(result.path), + label: result.label || basename(result.path), description: formattedPath, } })