From 93f88b45b6192b51cebe9c207386b2cfa4754dd4 Mon Sep 17 00:00:00 2001 From: Murilo Pires <50873657+MuriloFP@users.noreply.github.com> Date: Tue, 15 Jul 2025 19:25:43 -0300 Subject: [PATCH 1/7] feat: Add configurable error & repetition limit with unified control (#5654) (#5752) Co-authored-by: Daniel Riccio --- packages/types/src/provider-settings.ts | 6 ++ src/core/config/ProviderSettingsManager.ts | 22 +++++++ .../__tests__/ProviderSettingsManager.spec.ts | 42 ++++++++++++ src/core/task/Task.ts | 7 +- src/core/task/__tests__/Task.spec.ts | 64 +++++++++++++++++++ src/core/tools/ToolRepetitionDetector.ts | 7 +- .../__tests__/ToolRepetitionDetector.spec.ts | 56 ++++++++++++++++ src/core/webview/ClineProvider.ts | 2 + .../src/components/settings/ApiOptions.tsx | 10 +++ .../ConsecutiveMistakeLimitControl.tsx | 50 +++++++++++++++ webview-ui/src/i18n/locales/ca/settings.json | 6 ++ webview-ui/src/i18n/locales/de/settings.json | 6 ++ webview-ui/src/i18n/locales/en/settings.json | 6 ++ webview-ui/src/i18n/locales/es/settings.json | 6 ++ webview-ui/src/i18n/locales/fr/settings.json | 6 ++ webview-ui/src/i18n/locales/hi/settings.json | 6 ++ webview-ui/src/i18n/locales/id/settings.json | 6 ++ webview-ui/src/i18n/locales/it/settings.json | 6 ++ webview-ui/src/i18n/locales/ja/settings.json | 6 ++ webview-ui/src/i18n/locales/ko/settings.json | 6 ++ webview-ui/src/i18n/locales/nl/settings.json | 6 ++ webview-ui/src/i18n/locales/pl/settings.json | 6 ++ .../src/i18n/locales/pt-BR/settings.json | 6 ++ webview-ui/src/i18n/locales/ru/settings.json | 6 ++ webview-ui/src/i18n/locales/tr/settings.json | 6 ++ webview-ui/src/i18n/locales/vi/settings.json | 6 ++ .../src/i18n/locales/zh-CN/settings.json | 6 ++ .../src/i18n/locales/zh-TW/settings.json | 6 ++ 28 files changed, 369 insertions(+), 5 deletions(-) create mode 100644 webview-ui/src/components/settings/ConsecutiveMistakeLimitControl.tsx diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index 4cf4b30972..3b53627295 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -53,12 +53,18 @@ export type ProviderSettingsEntry = z.infer * ProviderSettings */ +/** + * Default value for consecutive mistake limit + */ +export const DEFAULT_CONSECUTIVE_MISTAKE_LIMIT = 3 + const baseProviderSettingsSchema = z.object({ includeMaxTokens: z.boolean().optional(), diffEnabled: z.boolean().optional(), fuzzyMatchThreshold: z.number().optional(), modelTemperature: z.number().nullish(), rateLimitSeconds: z.number().optional(), + consecutiveMistakeLimit: z.number().min(0).optional(), // Model reasoning. enableReasoningEffort: z.boolean().optional(), diff --git a/src/core/config/ProviderSettingsManager.ts b/src/core/config/ProviderSettingsManager.ts index 32c0135d3b..7823a3040a 100644 --- a/src/core/config/ProviderSettingsManager.ts +++ b/src/core/config/ProviderSettingsManager.ts @@ -5,6 +5,7 @@ import { type ProviderSettingsEntry, providerSettingsSchema, providerSettingsSchemaDiscriminated, + DEFAULT_CONSECUTIVE_MISTAKE_LIMIT, } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" @@ -26,6 +27,7 @@ export const providerProfilesSchema = z.object({ rateLimitSecondsMigrated: z.boolean().optional(), diffSettingsMigrated: z.boolean().optional(), openAiHeadersMigrated: z.boolean().optional(), + consecutiveMistakeLimitMigrated: z.boolean().optional(), }) .optional(), }) @@ -48,6 +50,7 @@ export class ProviderSettingsManager { rateLimitSecondsMigrated: true, // Mark as migrated on fresh installs diffSettingsMigrated: true, // Mark as migrated on fresh installs openAiHeadersMigrated: true, // Mark as migrated on fresh installs + consecutiveMistakeLimitMigrated: true, // Mark as migrated on fresh installs }, } @@ -113,6 +116,7 @@ export class ProviderSettingsManager { rateLimitSecondsMigrated: false, diffSettingsMigrated: false, openAiHeadersMigrated: false, + consecutiveMistakeLimitMigrated: false, } // Initialize with default values isDirty = true } @@ -135,6 +139,12 @@ export class ProviderSettingsManager { isDirty = true } + if (!providerProfiles.migrations.consecutiveMistakeLimitMigrated) { + await this.migrateConsecutiveMistakeLimit(providerProfiles) + providerProfiles.migrations.consecutiveMistakeLimitMigrated = true + isDirty = true + } + if (isDirty) { await this.store(providerProfiles) } @@ -228,6 +238,18 @@ export class ProviderSettingsManager { } } + private async migrateConsecutiveMistakeLimit(providerProfiles: ProviderProfiles) { + try { + for (const [name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) { + if (apiConfig.consecutiveMistakeLimit == null) { + apiConfig.consecutiveMistakeLimit = DEFAULT_CONSECUTIVE_MISTAKE_LIMIT + } + } + } catch (error) { + console.error(`[MigrateConsecutiveMistakeLimit] Failed to migrate consecutive mistake limit:`, error) + } + } + /** * List all available configs with metadata. */ diff --git a/src/core/config/__tests__/ProviderSettingsManager.spec.ts b/src/core/config/__tests__/ProviderSettingsManager.spec.ts index 6c37d733c4..6d24c63101 100644 --- a/src/core/config/__tests__/ProviderSettingsManager.spec.ts +++ b/src/core/config/__tests__/ProviderSettingsManager.spec.ts @@ -66,6 +66,7 @@ describe("ProviderSettingsManager", () => { rateLimitSecondsMigrated: true, diffSettingsMigrated: true, openAiHeadersMigrated: true, + consecutiveMistakeLimitMigrated: true, }, }), ) @@ -144,6 +145,47 @@ describe("ProviderSettingsManager", () => { expect(storedConfig.apiConfigs.existing.rateLimitSeconds).toEqual(43) }) + it("should call migrateConsecutiveMistakeLimit if it has not done so already", async () => { + mockSecrets.get.mockResolvedValue( + JSON.stringify({ + currentApiConfigName: "default", + apiConfigs: { + default: { + config: {}, + id: "default", + consecutiveMistakeLimit: undefined, + }, + test: { + apiProvider: "anthropic", + consecutiveMistakeLimit: undefined, + }, + existing: { + apiProvider: "anthropic", + // this should not really be possible, unless someone has loaded a hand edited config, + // but we don't overwrite so we'll check that + consecutiveMistakeLimit: 5, + }, + }, + migrations: { + rateLimitSecondsMigrated: true, + diffSettingsMigrated: true, + openAiHeadersMigrated: true, + consecutiveMistakeLimitMigrated: false, + }, + }), + ) + + await providerSettingsManager.initialize() + + // Get the last call to store, which should contain the migrated config + const calls = mockSecrets.store.mock.calls + const storedConfig = JSON.parse(calls[calls.length - 1][1]) + expect(storedConfig.apiConfigs.default.consecutiveMistakeLimit).toEqual(3) + expect(storedConfig.apiConfigs.test.consecutiveMistakeLimit).toEqual(3) + expect(storedConfig.apiConfigs.existing.consecutiveMistakeLimit).toEqual(5) + expect(storedConfig.migrations.consecutiveMistakeLimitMigrated).toEqual(true) + }) + it("should throw error if secrets storage fails", async () => { mockSecrets.get.mockRejectedValue(new Error("Storage failed")) diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index aa0590fedd..8a1bf1101d 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -18,6 +18,7 @@ import { type ClineMessage, type ClineSay, type ToolProgressStatus, + DEFAULT_CONSECUTIVE_MISTAKE_LIMIT, type HistoryItem, TelemetryEventName, TodoItem, @@ -216,7 +217,7 @@ export class Task extends EventEmitter { enableDiff = false, enableCheckpoints = true, fuzzyMatchThreshold = 1.0, - consecutiveMistakeLimit = 3, + consecutiveMistakeLimit = DEFAULT_CONSECUTIVE_MISTAKE_LIMIT, task, images, historyItem, @@ -255,7 +256,7 @@ export class Task extends EventEmitter { this.browserSession = new BrowserSession(provider.context) this.diffEnabled = enableDiff this.fuzzyMatchThreshold = fuzzyMatchThreshold - this.consecutiveMistakeLimit = consecutiveMistakeLimit + this.consecutiveMistakeLimit = consecutiveMistakeLimit ?? DEFAULT_CONSECUTIVE_MISTAKE_LIMIT this.providerRef = new WeakRef(provider) this.globalStoragePath = provider.context.globalStorageUri.fsPath this.diffViewProvider = new DiffViewProvider(this.cwd) @@ -1159,7 +1160,7 @@ export class Task extends EventEmitter { throw new Error(`[RooCode#recursivelyMakeRooRequests] task ${this.taskId}.${this.instanceId} aborted`) } - if (this.consecutiveMistakeCount >= this.consecutiveMistakeLimit) { + if (this.consecutiveMistakeLimit > 0 && this.consecutiveMistakeCount >= this.consecutiveMistakeLimit) { const { response, text, images } = await this.ask( "mistake_limit_reached", t("common:errors.mistake_limit_guidance"), diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index 693f72d1c7..797714cde8 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -320,6 +320,70 @@ describe("Cline", () => { expect(cline.diffStrategy).toBeDefined() }) + it("should use default consecutiveMistakeLimit when not provided", () => { + const cline = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "test task", + startTask: false, + }) + + expect(cline.consecutiveMistakeLimit).toBe(3) + }) + + it("should respect provided consecutiveMistakeLimit", () => { + const cline = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + consecutiveMistakeLimit: 5, + task: "test task", + startTask: false, + }) + + expect(cline.consecutiveMistakeLimit).toBe(5) + }) + + it("should keep consecutiveMistakeLimit of 0 as 0 for unlimited", () => { + const cline = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + consecutiveMistakeLimit: 0, + task: "test task", + startTask: false, + }) + + expect(cline.consecutiveMistakeLimit).toBe(0) + }) + + it("should pass 0 to ToolRepetitionDetector for unlimited mode", () => { + const cline = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + consecutiveMistakeLimit: 0, + task: "test task", + startTask: false, + }) + + // The toolRepetitionDetector should be initialized with 0 for unlimited mode + expect(cline.toolRepetitionDetector).toBeDefined() + // Verify the limit remains as 0 + expect(cline.consecutiveMistakeLimit).toBe(0) + }) + + it("should pass consecutiveMistakeLimit to ToolRepetitionDetector", () => { + const cline = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + consecutiveMistakeLimit: 5, + task: "test task", + startTask: false, + }) + + // The toolRepetitionDetector should be initialized with the same limit + expect(cline.toolRepetitionDetector).toBeDefined() + expect(cline.consecutiveMistakeLimit).toBe(5) + }) + it("should require either task or historyItem", () => { expect(() => { new Task({ provider: mockProvider, apiConfiguration: mockApiConfig }) diff --git a/src/core/tools/ToolRepetitionDetector.ts b/src/core/tools/ToolRepetitionDetector.ts index a82574ba0e..927b031e3b 100644 --- a/src/core/tools/ToolRepetitionDetector.ts +++ b/src/core/tools/ToolRepetitionDetector.ts @@ -43,8 +43,11 @@ export class ToolRepetitionDetector { this.previousToolCallJson = currentToolCallJson } - // Check if limit is reached - if (this.consecutiveIdenticalToolCallCount >= this.consecutiveIdenticalToolCallLimit) { + // Check if limit is reached (0 means unlimited) + if ( + this.consecutiveIdenticalToolCallLimit > 0 && + this.consecutiveIdenticalToolCallCount >= this.consecutiveIdenticalToolCallLimit + ) { // Reset counters to allow recovery if user guides the AI past this point this.consecutiveIdenticalToolCallCount = 0 this.previousToolCallJson = null diff --git a/src/core/tools/__tests__/ToolRepetitionDetector.spec.ts b/src/core/tools/__tests__/ToolRepetitionDetector.spec.ts index 972d401141..42041c1a46 100644 --- a/src/core/tools/__tests__/ToolRepetitionDetector.spec.ts +++ b/src/core/tools/__tests__/ToolRepetitionDetector.spec.ts @@ -301,5 +301,61 @@ describe("ToolRepetitionDetector", () => { expect(result3.allowExecution).toBe(false) expect(result3.askUser).toBeDefined() }) + + it("should never block when limit is 0 (unlimited)", () => { + const detector = new ToolRepetitionDetector(0) + + // Try many identical calls + for (let i = 0; i < 10; i++) { + const result = detector.check(createToolUse("tool", "tool-name")) + expect(result.allowExecution).toBe(true) + expect(result.askUser).toBeUndefined() + } + }) + + it("should handle different limits correctly", () => { + // Test with limit of 5 + const detector5 = new ToolRepetitionDetector(5) + const tool = createToolUse("tool", "tool-name") + + // First 4 calls should be allowed + for (let i = 0; i < 4; i++) { + const result = detector5.check(tool) + expect(result.allowExecution).toBe(true) + expect(result.askUser).toBeUndefined() + } + + // 5th call should be blocked + const result5 = detector5.check(tool) + expect(result5.allowExecution).toBe(false) + expect(result5.askUser).toBeDefined() + expect(result5.askUser?.messageKey).toBe("mistake_limit_reached") + }) + + it("should reset counter after blocking and allow new attempts", () => { + const detector = new ToolRepetitionDetector(2) + const tool = createToolUse("tool", "tool-name") + + // First call allowed + expect(detector.check(tool).allowExecution).toBe(true) + + // Second call should block (limit is 2) + const blocked = detector.check(tool) + expect(blocked.allowExecution).toBe(false) + + // After blocking, counter should reset and allow new attempts + expect(detector.check(tool).allowExecution).toBe(true) + }) + + it("should handle negative limits as 0 (unlimited)", () => { + const detector = new ToolRepetitionDetector(-1) + + // Should behave like unlimited + for (let i = 0; i < 5; i++) { + const result = detector.check(createToolUse("tool", "tool-name")) + expect(result.allowExecution).toBe(true) + expect(result.askUser).toBeUndefined() + } + }) }) }) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 233bd46926..107122dcb4 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -553,6 +553,7 @@ export class ClineProvider enableDiff, enableCheckpoints, fuzzyMatchThreshold, + consecutiveMistakeLimit: apiConfiguration.consecutiveMistakeLimit, task, images, experiments, @@ -589,6 +590,7 @@ export class ClineProvider enableDiff, enableCheckpoints, fuzzyMatchThreshold, + consecutiveMistakeLimit: apiConfiguration.consecutiveMistakeLimit, historyItem, experiments, rootTask: historyItem.rootTask, diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 5f86929043..e3f5ae7a0d 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -6,6 +6,7 @@ import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" import { type ProviderName, type ProviderSettings, + DEFAULT_CONSECUTIVE_MISTAKE_LIMIT, openRouterDefaultModelId, requestyDefaultModelId, glamaDefaultModelId, @@ -64,6 +65,7 @@ import { ThinkingBudget } from "./ThinkingBudget" import { DiffSettingsControl } from "./DiffSettingsControl" import { TemperatureControl } from "./TemperatureControl" import { RateLimitSecondsControl } from "./RateLimitSecondsControl" +import { ConsecutiveMistakeLimitControl } from "./ConsecutiveMistakeLimitControl" import { BedrockCustomArn } from "./providers/BedrockCustomArn" import { buildDocLink } from "@src/utils/docLinks" @@ -547,6 +549,14 @@ const ApiOptions = ({ value={apiConfiguration.rateLimitSeconds || 0} onChange={(value) => setApiConfigurationField("rateLimitSeconds", value)} /> + setApiConfigurationField("consecutiveMistakeLimit", value)} + /> )} diff --git a/webview-ui/src/components/settings/ConsecutiveMistakeLimitControl.tsx b/webview-ui/src/components/settings/ConsecutiveMistakeLimitControl.tsx new file mode 100644 index 0000000000..e60b2db323 --- /dev/null +++ b/webview-ui/src/components/settings/ConsecutiveMistakeLimitControl.tsx @@ -0,0 +1,50 @@ +import React, { useCallback } from "react" +import { Slider } from "@/components/ui" +import { useAppTranslation } from "@/i18n/TranslationContext" +import { DEFAULT_CONSECUTIVE_MISTAKE_LIMIT } from "@roo-code/types" + +interface ConsecutiveMistakeLimitControlProps { + value: number + onChange: (value: number) => void +} + +export const ConsecutiveMistakeLimitControl: React.FC = ({ value, onChange }) => { + const { t } = useAppTranslation() + + const handleValueChange = useCallback( + (newValue: number) => { + // Ensure value is not negative + const validValue = Math.max(0, newValue) + onChange(validValue) + }, + [onChange], + ) + + return ( +
+ +
+ handleValueChange(newValue[0])} + /> + {Math.max(0, value ?? DEFAULT_CONSECUTIVE_MISTAKE_LIMIT)} +
+
+ {value === 0 + ? t("settings:providers.consecutiveMistakeLimit.unlimitedDescription") + : t("settings:providers.consecutiveMistakeLimit.description", { + value: value ?? DEFAULT_CONSECUTIVE_MISTAKE_LIMIT, + })} +
+ {value === 0 && ( +
+ {t("settings:providers.consecutiveMistakeLimit.warning")} +
+ )} +
+ ) +} diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 0dd08afb29..503b8c36ed 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -367,6 +367,12 @@ "label": "Límit de freqüència", "description": "Temps mínim entre sol·licituds d'API." }, + "consecutiveMistakeLimit": { + "label": "Límit d'errors i repeticions", + "description": "Nombre d'errors consecutius o accions repetides abans de mostrar el diàleg 'En Roo està tenint problemes'", + "unlimitedDescription": "Reintents il·limitats habilitats (procediment automàtic). El diàleg no apareixerà mai.", + "warning": "⚠️ Establir a 0 permet reintents il·limitats que poden consumir un ús significatiu de l'API" + }, "reasoningEffort": { "label": "Esforç de raonament del model", "high": "Alt", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 923475d3fd..48d0d41a41 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -367,6 +367,12 @@ "label": "Ratenbegrenzung", "description": "Minimale Zeit zwischen API-Anfragen." }, + "consecutiveMistakeLimit": { + "label": "Fehler- & Wiederholungslimit", + "description": "Anzahl aufeinanderfolgender Fehler oder wiederholter Aktionen, bevor der Dialog 'Roo hat Probleme' angezeigt wird", + "unlimitedDescription": "Unbegrenzte Wiederholungen aktiviert (automatisches Fortfahren). Der Dialog wird niemals angezeigt.", + "warning": "⚠️ Das Setzen auf 0 erlaubt unbegrenzte Wiederholungen, was zu erheblichem API-Verbrauch führen kann" + }, "reasoningEffort": { "label": "Modell-Denkaufwand", "high": "Hoch", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 25428cfb16..69ca01154d 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -367,6 +367,12 @@ "label": "Rate limit", "description": "Minimum time between API requests." }, + "consecutiveMistakeLimit": { + "label": "Error & Repetition Limit", + "description": "Number of consecutive errors or repeated actions before showing 'Roo is having trouble' dialog", + "unlimitedDescription": "Unlimited retries enabled (auto-proceed). The dialog will never appear.", + "warning": "⚠️ Setting to 0 allows unlimited retries which may consume significant API usage" + }, "reasoningEffort": { "label": "Model Reasoning Effort", "high": "High", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 7f6af9bd72..684949488e 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -367,6 +367,12 @@ "label": "Límite de tasa", "description": "Tiempo mínimo entre solicitudes de API." }, + "consecutiveMistakeLimit": { + "label": "Límite de errores y repeticiones", + "description": "Número de errores consecutivos o acciones repetidas antes de mostrar el diálogo 'Roo está teniendo problemas'", + "unlimitedDescription": "Reintentos ilimitados habilitados (proceder automáticamente). El diálogo nunca aparecerá.", + "warning": "⚠️ Establecer en 0 permite reintentos ilimitados que pueden consumir un uso significativo de la API" + }, "reasoningEffort": { "label": "Esfuerzo de razonamiento del modelo", "high": "Alto", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 5986a5bf17..8080158446 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -367,6 +367,12 @@ "label": "Limite de débit", "description": "Temps minimum entre les requêtes API." }, + "consecutiveMistakeLimit": { + "label": "Limite d'erreurs et de répétitions", + "description": "Nombre d'erreurs consécutives ou d'actions répétées avant d'afficher la boîte de dialogue 'Roo a des difficultés'", + "unlimitedDescription": "Réessais illimités activés (poursuite automatique). La boîte de dialogue n'apparaîtra jamais.", + "warning": "⚠️ Mettre à 0 autorise des réessais illimités, ce qui peut consommer une utilisation importante de l'API" + }, "reasoningEffort": { "label": "Effort de raisonnement du modèle", "high": "Élevé", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 0a99064588..30a34de434 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -367,6 +367,12 @@ "label": "दर सीमा", "description": "API अनुरोधों के बीच न्यूनतम समय।" }, + "consecutiveMistakeLimit": { + "label": "त्रुटि और पुनरावृत्ति सीमा", + "description": "'रू को समस्या हो रही है' संवाद दिखाने से पहले लगातार त्रुटियों या दोहराए गए कार्यों की संख्या", + "unlimitedDescription": "असीमित पुनः प्रयास सक्षम (स्वतः आगे बढ़ें)। संवाद कभी नहीं दिखाई देगा।", + "warning": "⚠️ 0 पर सेट करने से असीमित पुनः प्रयास की अनुमति मिलती है जिससे महत्वपूर्ण एपीआई उपयोग हो सकता है" + }, "reasoningEffort": { "label": "मॉडल तर्क प्रयास", "high": "उच्च", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 9ae5dd846c..6089d5874e 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -371,6 +371,12 @@ "label": "Rate limit", "description": "Waktu minimum antara permintaan API." }, + "consecutiveMistakeLimit": { + "label": "Batas Kesalahan & Pengulangan", + "description": "Jumlah kesalahan berturut-turut atau tindakan berulang sebelum menampilkan dialog 'Roo mengalami masalah'", + "unlimitedDescription": "Percobaan ulang tak terbatas diaktifkan (lanjut otomatis). Dialog tidak akan pernah muncul.", + "warning": "⚠️ Mengatur ke 0 memungkinkan percobaan ulang tak terbatas yang dapat menghabiskan penggunaan API yang signifikan" + }, "reasoningEffort": { "label": "Upaya Reasoning Model", "high": "Tinggi", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index a2897496ec..189efe0bed 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -367,6 +367,12 @@ "label": "Limite di frequenza", "description": "Tempo minimo tra le richieste API." }, + "consecutiveMistakeLimit": { + "label": "Limite di errori e ripetizioni", + "description": "Numero di errori consecutivi o azioni ripetute prima di mostrare la finestra di dialogo 'Roo sta riscontrando problemi'", + "unlimitedDescription": "Tentativi illimitati abilitati (procedi automaticamente). La finestra di dialogo non verrà mai visualizzata.", + "warning": "⚠️ L'impostazione a 0 consente tentativi illimitati che possono consumare un notevole utilizzo dell'API" + }, "reasoningEffort": { "label": "Sforzo di ragionamento del modello", "high": "Alto", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index bda4182851..a63d0b6800 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -367,6 +367,12 @@ "label": "レート制限", "description": "APIリクエスト間の最小時間。" }, + "consecutiveMistakeLimit": { + "label": "エラーと繰り返しの制限", + "description": "「Rooが問題を抱えています」ダイアログを表示するまでの連続エラーまたは繰り返しアクションの数", + "unlimitedDescription": "無制限のリトライが有効です(自動進行)。ダイアログは表示されません。", + "warning": "⚠️ 0に設定すると無制限のリトライが可能になり、API使用量が大幅に増加する可能性があります" + }, "reasoningEffort": { "label": "モデル推論の労力", "high": "高", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 5053ed62f3..c4da7c8602 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -367,6 +367,12 @@ "label": "속도 제한", "description": "API 요청 간 최소 시간." }, + "consecutiveMistakeLimit": { + "label": "오류 및 반복 제한", + "description": "'Roo에 문제가 발생했습니다' 대화 상자를 표시하기 전의 연속 오류 또는 반복 작업 수", + "unlimitedDescription": "무제한 재시도 활성화 (자동 진행). 대화 상자가 나타나지 않습니다.", + "warning": "⚠️ 0으로 설정하면 무제한 재시도가 허용되어 상당한 API 사용량이 발생할 수 있습니다" + }, "reasoningEffort": { "label": "모델 추론 노력", "high": "높음", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index ab12e1931c..535c462fe2 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -367,6 +367,12 @@ "label": "Snelheidslimiet", "description": "Minimale tijd tussen API-verzoeken." }, + "consecutiveMistakeLimit": { + "label": "Fout- & Herhalingslimiet", + "description": "Aantal opeenvolgende fouten of herhaalde acties voordat het dialoogvenster 'Roo ondervindt problemen' wordt weergegeven", + "unlimitedDescription": "Onbeperkt aantal nieuwe pogingen ingeschakeld (automatisch doorgaan). Het dialoogvenster zal nooit verschijnen.", + "warning": "⚠️ Instellen op 0 staat onbeperkte nieuwe pogingen toe, wat aanzienlijk API-gebruik kan verbruiken" + }, "reasoningEffort": { "label": "Model redeneervermogen", "high": "Hoog", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 266c668606..f7d80cd1a6 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -367,6 +367,12 @@ "label": "Limit szybkości", "description": "Minimalny czas między żądaniami API." }, + "consecutiveMistakeLimit": { + "label": "Limit błędów i powtórzeń", + "description": "Liczba kolejnych błędów lub powtórzonych akcji przed wyświetleniem okna dialogowego 'Roo ma problemy'", + "unlimitedDescription": "Włączono nieograniczone próby (automatyczne kontynuowanie). Okno dialogowe nigdy się nie pojawi.", + "warning": "⚠️ Ustawienie na 0 pozwala na nieograniczone próby, co może zużyć znaczną ilość API" + }, "reasoningEffort": { "label": "Wysiłek rozumowania modelu", "high": "Wysoki", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index e9825ae749..a9ccd98838 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -367,6 +367,12 @@ "label": "Limite de taxa", "description": "Tempo mínimo entre requisições de API." }, + "consecutiveMistakeLimit": { + "label": "Limite de Erros e Repetições", + "description": "Número de erros consecutivos ou ações repetidas antes de exibir o diálogo 'Roo está com problemas'", + "unlimitedDescription": "Tentativas ilimitadas ativadas (prosseguimento automático). O diálogo nunca aparecerá.", + "warning": "⚠️ Definir como 0 permite tentativas ilimitadas, o que pode consumir um uso significativo da API" + }, "reasoningEffort": { "label": "Esforço de raciocínio do modelo", "high": "Alto", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 1b74b2253f..696c5ef641 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -367,6 +367,12 @@ "label": "Лимит скорости", "description": "Минимальное время между запросами к API." }, + "consecutiveMistakeLimit": { + "label": "Лимит ошибок и повторений", + "description": "Количество последовательных ошибок или повторных действий перед показом диалогового окна 'У Roo возникли проблемы'", + "unlimitedDescription": "Включены неограниченные повторные попытки (автоматическое продолжение). Диалоговое окно никогда не появится.", + "warning": "⚠️ Установка значения 0 разрешает неограниченные повторные попытки, что может значительно увеличить использование API" + }, "reasoningEffort": { "label": "Усилия по рассуждению модели", "high": "Высокие", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 49d1803ddf..f81bfd98ea 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -367,6 +367,12 @@ "label": "Hız sınırı", "description": "API istekleri arasındaki minimum süre." }, + "consecutiveMistakeLimit": { + "label": "Hata ve Tekrar Limiti", + "description": "'Roo sorun yaşıyor' iletişim kutusunu göstermeden önceki ardışık hata veya tekrarlanan eylem sayısı", + "unlimitedDescription": "Sınırsız yeniden deneme etkin (otomatik devam et). Diyalog asla görünmeyecek.", + "warning": "⚠️ 0'a ayarlamak, önemli API kullanımına neden olabilecek sınırsız yeniden denemeye izin verir" + }, "reasoningEffort": { "label": "Model Akıl Yürütme Çabası", "high": "Yüksek", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index c12b5778a2..97f24b4733 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -367,6 +367,12 @@ "label": "Giới hạn tốc độ", "description": "Thời gian tối thiểu giữa các yêu cầu API." }, + "consecutiveMistakeLimit": { + "label": "Giới hạn lỗi và lặp lại", + "description": "Số lỗi liên tiếp hoặc hành động lặp lại trước khi hiển thị hộp thoại 'Roo đang gặp sự cố'", + "unlimitedDescription": "Đã bật thử lại không giới hạn (tự động tiếp tục). Hộp thoại sẽ không bao giờ xuất hiện.", + "warning": "⚠️ Đặt thành 0 cho phép thử lại không giới hạn, điều này có thể tiêu tốn mức sử dụng API đáng kể" + }, "reasoningEffort": { "label": "Nỗ lực suy luận của mô hình", "high": "Cao", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index c75578067d..bbe85d0e84 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -367,6 +367,12 @@ "label": "API 请求频率限制", "description": "设置API请求的最小间隔时间" }, + "consecutiveMistakeLimit": { + "label": "错误和重复限制", + "description": "在显示“Roo遇到问题”对话框前允许的连续错误或重复操作次数", + "unlimitedDescription": "已启用无限重试(自动继续)。对话框将永远不会出现。", + "warning": "⚠️ 设置为 0 允许无限重试,这可能会消耗大量 API 使用量" + }, "reasoningEffort": { "label": "模型推理强度", "high": "高", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index dec9e936b2..4ca64bbc5c 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -367,6 +367,12 @@ "label": "速率限制", "description": "API 請求間的最短時間" }, + "consecutiveMistakeLimit": { + "label": "錯誤和重複限制", + "description": "在顯示「Roo 遇到問題」對話方塊前允許的連續錯誤或重複操作次數", + "unlimitedDescription": "已啟用無限重試(自動繼續)。對話方塊將永遠不會出現。", + "warning": "⚠️ 設定為 0 允許無限重試,這可能會消耗大量 API 使用量" + }, "reasoningEffort": { "label": "模型推理強度", "high": "高", From db718533c617716d4b8b91d1130034d340880592 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 15 Jul 2025 19:00:14 -0400 Subject: [PATCH 2/7] Move less commonly used provider settings into an advanced dropdown (#5762) --- .../src/components/settings/ApiOptions.tsx | 122 ++++++++++++++---- .../settings/__tests__/ApiOptions.spec.tsx | 14 ++ .../settings/__tests__/SettingsView.spec.tsx | 14 ++ .../settings/providers/OpenRouter.tsx | 50 ------- webview-ui/src/i18n/locales/ca/settings.json | 3 + webview-ui/src/i18n/locales/de/settings.json | 3 + webview-ui/src/i18n/locales/en/settings.json | 3 + webview-ui/src/i18n/locales/es/settings.json | 3 + webview-ui/src/i18n/locales/fr/settings.json | 3 + webview-ui/src/i18n/locales/hi/settings.json | 3 + webview-ui/src/i18n/locales/id/settings.json | 3 + webview-ui/src/i18n/locales/it/settings.json | 3 + webview-ui/src/i18n/locales/ja/settings.json | 3 + webview-ui/src/i18n/locales/ko/settings.json | 3 + webview-ui/src/i18n/locales/nl/settings.json | 3 + webview-ui/src/i18n/locales/pl/settings.json | 3 + .../src/i18n/locales/pt-BR/settings.json | 3 + webview-ui/src/i18n/locales/ru/settings.json | 3 + webview-ui/src/i18n/locales/tr/settings.json | 3 + webview-ui/src/i18n/locales/vi/settings.json | 3 + .../src/i18n/locales/zh-CN/settings.json | 3 + .../src/i18n/locales/zh-TW/settings.json | 3 + 22 files changed, 179 insertions(+), 75 deletions(-) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index e3f5ae7a0d..d0ec50abad 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -2,6 +2,7 @@ import React, { memo, useCallback, useEffect, useMemo, useState } from "react" import { convertHeadersToObject } from "./utils/headers" import { useDebounce } from "react-use" import { VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import { ExternalLinkIcon } from "@radix-ui/react-icons" import { type ProviderName, @@ -31,8 +32,22 @@ import { useAppTranslation } from "@src/i18n/TranslationContext" import { useRouterModels } from "@src/components/ui/hooks/useRouterModels" import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel" import { useExtensionState } from "@src/context/ExtensionStateContext" +import { + useOpenRouterModelProviders, + OPENROUTER_DEFAULT_PROVIDER_NAME, +} from "@src/components/ui/hooks/useOpenRouterModelProviders" import { filterProviders, filterModels } from "./utils/organizationFilters" -import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, SearchableSelect } from "@src/components/ui" +import { + Select, + SelectTrigger, + SelectValue, + SelectContent, + SelectItem, + SearchableSelect, + Collapsible, + CollapsibleTrigger, + CollapsibleContent, +} from "@src/components/ui" import { Anthropic, @@ -121,6 +136,7 @@ const ApiOptions = ({ ) const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false) + const [isAdvancedSettingsOpen, setIsAdvancedSettingsOpen] = useState(false) const handleInputChange = useCallback( ( @@ -141,6 +157,14 @@ const ApiOptions = ({ const { data: routerModels, refetch: refetchRouterModels } = useRouterModels() + const { data: openRouterModelProviders } = useOpenRouterModelProviders(apiConfiguration?.openRouterModelId, { + enabled: + !!apiConfiguration?.openRouterModelId && + routerModels?.openrouter && + Object.keys(routerModels.openrouter).length > 1 && + apiConfiguration.openRouterModelId in routerModels.openrouter, + }) + // Update `apiModelId` whenever `selectedModelId` changes. useEffect(() => { if (selectedModelId) { @@ -534,30 +558,78 @@ const ApiOptions = ({ /> {!fromWelcomeView && ( - <> - setApiConfigurationField(field, value)} - /> - - setApiConfigurationField("rateLimitSeconds", value)} - /> - setApiConfigurationField("consecutiveMistakeLimit", value)} - /> - + + + + {t("settings:advancedSettings.title")} + + + setApiConfigurationField(field, value)} + /> + + setApiConfigurationField("rateLimitSeconds", value)} + /> + setApiConfigurationField("consecutiveMistakeLimit", value)} + /> + {selectedProvider === "openrouter" && + openRouterModelProviders && + Object.keys(openRouterModelProviders).length > 0 && ( +
+
+ + + + +
+ +
+ {t("settings:providers.openRouter.providerRouting.description")}{" "} + + {t("settings:providers.openRouter.providerRouting.learnMore")}. + +
+
+ )} +
+
)} ) diff --git a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx index 07331b8e42..bf02840429 100644 --- a/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/ApiOptions.spec.tsx @@ -100,6 +100,20 @@ vi.mock("@/components/ui", () => ({ ), + // Add Collapsible components + Collapsible: ({ children, open }: any) => ( +
+ {children} +
+ ), + CollapsibleTrigger: ({ children, className, onClick }: any) => ( +
+ {children} +
+ ), + CollapsibleContent: ({ children, className }: any) => ( +
{children}
+ ), })) vi.mock("../TemperatureControl", () => ({ diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx index 404b3a6883..72d4598ea6 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx @@ -179,6 +179,20 @@ vi.mock("@/components/ui", () => ({ {children} ), + // Add Collapsible components + Collapsible: ({ children, open }: any) => ( +
+ {children} +
+ ), + CollapsibleTrigger: ({ children, className, onClick }: any) => ( +
+ {children} +
+ ), + CollapsibleContent: ({ children, className }: any) => ( +
{children}
+ ), })) // Mock window.postMessage to trigger state hydration diff --git a/webview-ui/src/components/settings/providers/OpenRouter.tsx b/webview-ui/src/components/settings/providers/OpenRouter.tsx index 9ef73d8b24..f6cad36bf8 100644 --- a/webview-ui/src/components/settings/providers/OpenRouter.tsx +++ b/webview-ui/src/components/settings/providers/OpenRouter.tsx @@ -2,7 +2,6 @@ import { useCallback, useState } from "react" import { Trans } from "react-i18next" import { Checkbox } from "vscrui" import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react" -import { ExternalLinkIcon } from "@radix-ui/react-icons" import { type ProviderSettings, type OrganizationAllowList, openRouterDefaultModelId } from "@roo-code/types" @@ -10,12 +9,7 @@ import type { RouterModels } from "@roo/api" import { useAppTranslation } from "@src/i18n/TranslationContext" import { getOpenRouterAuthUrl } from "@src/oauth/urls" -import { - useOpenRouterModelProviders, - OPENROUTER_DEFAULT_PROVIDER_NAME, -} from "@src/components/ui/hooks/useOpenRouterModelProviders" import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@src/components/ui" import { inputEventTransform, noTransform } from "../transforms" @@ -37,7 +31,6 @@ export const OpenRouter = ({ apiConfiguration, setApiConfigurationField, routerModels, - selectedModelId, uriScheme, fromWelcomeView, organizationAllowList, @@ -58,14 +51,6 @@ export const OpenRouter = ({ [setApiConfigurationField], ) - const { data: openRouterModelProviders } = useOpenRouterModelProviders(apiConfiguration?.openRouterModelId, { - enabled: - !!apiConfiguration?.openRouterModelId && - routerModels?.openrouter && - Object.keys(routerModels.openrouter).length > 1 && - apiConfiguration.openRouterModelId in routerModels.openrouter, - }) - return ( <> - {openRouterModelProviders && Object.keys(openRouterModelProviders).length > 0 && ( -
-
- - - - -
- -
- {t("settings:providers.openRouter.providerRouting.description")}{" "} - - {t("settings:providers.openRouter.providerRouting.learnMore")}. - -
-
- )} ) } diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 503b8c36ed..207fc88fd9 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -542,6 +542,9 @@ "description": "Quan està habilitat, el terminal hereta les variables d'entorn del procés pare de VSCode, com ara la configuració d'integració del shell definida al perfil d'usuari. Això commuta directament la configuració global de VSCode `terminal.integrated.inheritEnv`. <0>Més informació" } }, + "advancedSettings": { + "title": "Configuració avançada" + }, "advanced": { "diff": { "label": "Habilitar edició mitjançant diffs", diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 48d0d41a41..68e9f8f97d 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -542,6 +542,9 @@ "description": "Wenn aktiviert, erbt das Terminal Umgebungsvariablen aus dem übergeordneten Prozess von VSCode, wie z.B. benutzerdefinierte Shell-Integrationseinstellungen. Dies schaltet direkt die globale VSCode-Einstellung `terminal.integrated.inheritEnv` um. <0>Mehr erfahren" } }, + "advancedSettings": { + "title": "Erweiterte Einstellungen" + }, "advanced": { "diff": { "label": "Bearbeitung durch Diffs aktivieren", diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 69ca01154d..fa1fbab13c 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -542,6 +542,9 @@ "description": "When enabled, the terminal will inherit environment variables from VSCode's parent process, such as user-profile-defined shell integration settings. This directly toggles VSCode global setting `terminal.integrated.inheritEnv`. <0>Learn more" } }, + "advancedSettings": { + "title": "Advanced settings" + }, "advanced": { "diff": { "label": "Enable editing through diffs", diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 684949488e..80de7042b0 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -542,6 +542,9 @@ "description": "Cuando está habilitado, el terminal hereda las variables de entorno del proceso padre de VSCode, como la configuración de integración del shell definida en el perfil del usuario. Esto alterna directamente la configuración global de VSCode `terminal.integrated.inheritEnv`. <0>Más información" } }, + "advancedSettings": { + "title": "Configuración avanzada" + }, "advanced": { "diff": { "label": "Habilitar edición a través de diffs", diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 8080158446..e10b3ae35f 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -542,6 +542,9 @@ "description": "Lorsqu'activé, le terminal hérite des variables d'environnement du processus parent VSCode, comme les paramètres d'intégration du shell définis dans le profil utilisateur. Cela bascule directement le paramètre global VSCode `terminal.integrated.inheritEnv`. <0>En savoir plus" } }, + "advancedSettings": { + "title": "Paramètres avancés" + }, "advanced": { "diff": { "label": "Activer l'édition via des diffs", diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 30a34de434..5b64359f8f 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -542,6 +542,9 @@ "description": "सक्षम होने पर, टर्मिनल VSCode के मूल प्रक्रिया से पर्यावरण चर विरासत में लेता है, जैसे उपयोगकर्ता प्रोफ़ाइल में परिभाषित शेल एकीकरण सेटिंग्स। यह VSCode की वैश्विक सेटिंग `terminal.integrated.inheritEnv` को सीधे टॉगल करता है। <0>अधिक जानें" } }, + "advancedSettings": { + "title": "उन्नत सेटिंग्स" + }, "advanced": { "diff": { "label": "diffs के माध्यम से संपादन सक्षम करें", diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 6089d5874e..24404dec3d 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -546,6 +546,9 @@ "description": "Ketika diaktifkan, terminal akan mewarisi variabel environment dari proses parent VSCode, seperti pengaturan integrasi shell yang didefinisikan user-profile. Ini secara langsung mengalihkan pengaturan global VSCode `terminal.integrated.inheritEnv`. <0>Pelajari lebih lanjut" } }, + "advancedSettings": { + "title": "Pengaturan lanjutan" + }, "advanced": { "diff": { "label": "Aktifkan editing melalui diff", diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 189efe0bed..bacf7a40ef 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -542,6 +542,9 @@ "description": "Quando abilitato, il terminale eredita le variabili d'ambiente dal processo padre di VSCode, come le impostazioni di integrazione della shell definite nel profilo utente. Questo attiva direttamente l'impostazione globale di VSCode `terminal.integrated.inheritEnv`. <0>Scopri di più" } }, + "advancedSettings": { + "title": "Impostazioni avanzate" + }, "advanced": { "diff": { "label": "Abilita modifica tramite diff", diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index a63d0b6800..c588edead2 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -542,6 +542,9 @@ "description": "有効にすると、ターミナルは VSCode の親プロセスから環境変数を継承します。ユーザープロファイルで定義されたシェル統合設定などが含まれます。これは VSCode のグローバル設定 `terminal.integrated.inheritEnv` を直接切り替えます。 <0>詳細情報" } }, + "advancedSettings": { + "title": "詳細設定" + }, "advanced": { "diff": { "label": "diff経由の編集を有効化", diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index c4da7c8602..84c329ffd7 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -542,6 +542,9 @@ "description": "활성화하면 터미널이 VSCode 부모 프로세스로부터 환경 변수를 상속받습니다. 사용자 프로필에 정의된 셸 통합 설정 등이 포함됩니다. 이는 VSCode 전역 설정 `terminal.integrated.inheritEnv`를 직접 전환합니다. <0>더 알아보기" } }, + "advancedSettings": { + "title": "고급 설정" + }, "advanced": { "diff": { "label": "diff를 통한 편집 활성화", diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 535c462fe2..343008ae51 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -542,6 +542,9 @@ "description": "Indien ingeschakeld, neemt de terminal omgevingsvariabelen over van het bovenliggende VSCode-proces, zoals shell-integratie-instellingen uit het gebruikersprofiel. Dit schakelt direct de VSCode-instelling `terminal.integrated.inheritEnv` om. <0>Meer informatie" } }, + "advancedSettings": { + "title": "Geavanceerde instellingen" + }, "advanced": { "diff": { "label": "Bewerken via diffs inschakelen", diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index f7d80cd1a6..2f300384e4 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -542,6 +542,9 @@ "description": "Po włączeniu terminal dziedziczy zmienne środowiskowe z procesu nadrzędnego VSCode, takie jak ustawienia integracji powłoki zdefiniowane w profilu użytkownika. Przełącza to bezpośrednio globalne ustawienie VSCode `terminal.integrated.inheritEnv`. <0>Dowiedz się więcej" } }, + "advancedSettings": { + "title": "Ustawienia zaawansowane" + }, "advanced": { "diff": { "label": "Włącz edycję przez różnice", diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index a9ccd98838..11e4d01aac 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -542,6 +542,9 @@ "description": "Quando ativado, o terminal herda variáveis de ambiente do processo pai do VSCode, como configurações de integração do shell definidas no perfil do usuário. Isso alterna diretamente a configuração global do VSCode `terminal.integrated.inheritEnv`. <0>Saiba mais" } }, + "advancedSettings": { + "title": "Configurações avançadas" + }, "advanced": { "diff": { "label": "Ativar edição através de diffs", diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 696c5ef641..35eb9b1966 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -542,6 +542,9 @@ "description": "Если включено, терминал будет наследовать переменные среды от родительского процесса VSCode, такие как настройки интеграции оболочки, определённые в профиле пользователя. Напрямую переключает глобальную настройку VSCode `terminal.integrated.inheritEnv`. <0>Подробнее" } }, + "advancedSettings": { + "title": "Дополнительные настройки" + }, "advanced": { "diff": { "label": "Включить редактирование через диффы", diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index f81bfd98ea..9b90c97fb3 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -542,6 +542,9 @@ "description": "Etkinleştirildiğinde, terminal VSCode üst işleminden ortam değişkenlerini devralır, örneğin kullanıcı profilinde tanımlanan kabuk entegrasyon ayarları gibi. Bu, VSCode'un global ayarı olan `terminal.integrated.inheritEnv` değerini doğrudan değiştirir. <0>Daha fazla bilgi" } }, + "advancedSettings": { + "title": "Gelişmiş ayarlar" + }, "advanced": { "diff": { "label": "Diff'ler aracılığıyla düzenlemeyi etkinleştir", diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 97f24b4733..19b574af36 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -542,6 +542,9 @@ "description": "Khi được bật, terminal sẽ kế thừa các biến môi trường từ tiến trình cha của VSCode, như các cài đặt tích hợp shell được định nghĩa trong hồ sơ người dùng. Điều này trực tiếp chuyển đổi cài đặt toàn cục của VSCode `terminal.integrated.inheritEnv`. <0>Tìm hiểu thêm" } }, + "advancedSettings": { + "title": "Cài đặt nâng cao" + }, "advanced": { "diff": { "label": "Bật chỉnh sửa qua diff", diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index bbe85d0e84..402de1e683 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -542,6 +542,9 @@ "description": "启用后,终端将从 VSCode 父进程继承环境变量,如用户配置文件中定义的 shell 集成设置。这直接切换 VSCode 全局设置 `terminal.integrated.inheritEnv`。 <0>了解更多" } }, + "advancedSettings": { + "title": "高级设置" + }, "advanced": { "diff": { "label": "启用diff更新", diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 4ca64bbc5c..068e64d21f 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -542,6 +542,9 @@ "description": "啟用後,終端機將從 VSCode 父程序繼承環境變數,如使用者設定檔中定義的 shell 整合設定。這直接切換 VSCode 全域設定 `terminal.integrated.inheritEnv`。 <0>瞭解更多" } }, + "advancedSettings": { + "title": "進階設定" + }, "advanced": { "diff": { "label": "透過差異比對編輯", From d513b9c0e15f3e4b80ba4f5a539fa7862f5d3b06 Mon Sep 17 00:00:00 2001 From: Murilo Pires <50873657+MuriloFP@users.noreply.github.com> Date: Tue, 15 Jul 2025 20:50:55 -0300 Subject: [PATCH 3/7] fix: Exclude Terraform and Terragrunt cache directories from checkpoints (#4601) (#5750) * feat: add Issue Fixer Orchestrator mode * fix: Exclude Terraform and Terragrunt cache directories from checkpoints (#4601) --- src/services/checkpoints/excludes.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/checkpoints/excludes.ts b/src/services/checkpoints/excludes.ts index b7bbe81ae5..382e400f18 100644 --- a/src/services/checkpoints/excludes.ts +++ b/src/services/checkpoints/excludes.ts @@ -11,6 +11,8 @@ const getBuildArtifactPatterns = () => [ ".next/", ".nuxt/", ".sass-cache/", + ".terraform/", + ".terragrunt-cache/", ".vs/", ".vscode/", "Pods/", From 8c8888a977dd2a9e03428847cf83fe6e4036b4ee Mon Sep 17 00:00:00 2001 From: Daniel <57051444+daniel-lxs@users.noreply.github.com> Date: Wed, 16 Jul 2025 08:30:33 -0500 Subject: [PATCH 4/7] feat: increase Ollama API timeout values and extract as constants (#5778) - Increase embedding request timeout from 10s to 60s - Increase validation request timeouts from 5s to 30s - Extract timeout values as module-level constants for better maintainability - OLLAMA_EMBEDDING_TIMEOUT_MS = 60000 (60 seconds) - OLLAMA_VALIDATION_TIMEOUT_MS = 30000 (30 seconds) --- src/services/code-index/embedders/ollama.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/services/code-index/embedders/ollama.ts b/src/services/code-index/embedders/ollama.ts index 20b22b92bf..c160d39490 100644 --- a/src/services/code-index/embedders/ollama.ts +++ b/src/services/code-index/embedders/ollama.ts @@ -7,6 +7,10 @@ import { withValidationErrorHandling, sanitizeErrorMessage } from "../shared/val import { TelemetryService } from "@roo-code/telemetry" import { TelemetryEventName } from "@roo-code/types" +// Timeout constants for Ollama API requests +const OLLAMA_EMBEDDING_TIMEOUT_MS = 60000 // 60 seconds for embedding requests +const OLLAMA_VALIDATION_TIMEOUT_MS = 30000 // 30 seconds for validation requests + /** * Implements the IEmbedder interface using a local Ollama instance. */ @@ -61,7 +65,7 @@ export class CodeIndexOllamaEmbedder implements IEmbedder { // Add timeout to prevent indefinite hanging const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), 10000) // 10 second timeout + const timeoutId = setTimeout(() => controller.abort(), OLLAMA_EMBEDDING_TIMEOUT_MS) const response = await fetch(url, { method: "POST", @@ -140,7 +144,7 @@ export class CodeIndexOllamaEmbedder implements IEmbedder { // Add timeout to prevent indefinite hanging const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), 5000) // 5 second timeout + const timeoutId = setTimeout(() => controller.abort(), OLLAMA_VALIDATION_TIMEOUT_MS) const modelsResponse = await fetch(modelsUrl, { method: "GET", @@ -197,7 +201,7 @@ export class CodeIndexOllamaEmbedder implements IEmbedder { // Add timeout for test request too const testController = new AbortController() - const testTimeoutId = setTimeout(() => testController.abort(), 5000) + const testTimeoutId = setTimeout(() => testController.abort(), OLLAMA_VALIDATION_TIMEOUT_MS) const testResponse = await fetch(testUrl, { method: "POST", From 0f994fcf2285d350ac6dab59c1f6eb6000cb96b1 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Wed, 16 Jul 2025 10:57:34 -0400 Subject: [PATCH 5/7] Fix settings dirty check (#5779) --- webview-ui/src/components/settings/ApiOptions.tsx | 4 ++-- webview-ui/src/components/settings/SettingsView.tsx | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index d0ec50abad..06994b16b9 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -167,10 +167,10 @@ const ApiOptions = ({ // Update `apiModelId` whenever `selectedModelId` changes. useEffect(() => { - if (selectedModelId) { + if (selectedModelId && apiConfiguration.apiModelId !== selectedModelId) { setApiConfigurationField("apiModelId", selectedModelId) } - }, [selectedModelId, setApiConfigurationField]) + }, [selectedModelId, setApiConfigurationField, apiConfiguration.apiModelId]) // Debounced refresh model updates, only executed 250ms after the user // stops typing. diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index cf9e779cbd..fd3a8a129b 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -218,7 +218,15 @@ const SettingsView = forwardRef(({ onDone, t return prevState } - setChangeDetected(true) + const previousValue = prevState.apiConfiguration?.[field] + + // Don't treat initial sync from undefined to a defined value as a user change + // This prevents the dirty state when the component initializes and auto-syncs the model ID + const isInitialSync = previousValue === undefined && value !== undefined + + if (!isInitialSync) { + setChangeDetected(true) + } return { ...prevState, apiConfiguration: { ...prevState.apiConfiguration, [field]: value } } }) }, From a7a6bcb30f7972782c0f6c8f230d2f40767808ab Mon Sep 17 00:00:00 2001 From: Daniel <57051444+daniel-lxs@users.noreply.github.com> Date: Wed, 16 Jul 2025 14:06:26 -0500 Subject: [PATCH 6/7] fix: resolve DirectoryScanner memory leak and improve file limit handling (#5785) --- src/services/code-index/constants/index.ts | 2 +- .../code-index/interfaces/file-processor.ts | 1 - .../processors/__tests__/scanner.spec.ts | 36 +++++++------ src/services/code-index/processors/scanner.ts | 50 ++++++++++++------- 4 files changed, 55 insertions(+), 34 deletions(-) diff --git a/src/services/code-index/constants/index.ts b/src/services/code-index/constants/index.ts index c2567f5635..706a73935a 100644 --- a/src/services/code-index/constants/index.ts +++ b/src/services/code-index/constants/index.ts @@ -15,7 +15,7 @@ export const QDRANT_CODE_BLOCK_NAMESPACE = "f47ac10b-58cc-4372-a567-0e02b2c3d479 export const MAX_FILE_SIZE_BYTES = 1 * 1024 * 1024 // 1MB /**Directory Scanner */ -export const MAX_LIST_FILES_LIMIT = 3_000 +export const MAX_LIST_FILES_LIMIT_CODE_INDEX = 50_000 export const BATCH_SEGMENT_THRESHOLD = 60 // Number of code segments to batch for embeddings/upserts export const MAX_BATCH_RETRIES = 3 export const INITIAL_RETRY_DELAY_MS = 500 diff --git a/src/services/code-index/interfaces/file-processor.ts b/src/services/code-index/interfaces/file-processor.ts index f00c19c619..88b19007c3 100644 --- a/src/services/code-index/interfaces/file-processor.ts +++ b/src/services/code-index/interfaces/file-processor.ts @@ -38,7 +38,6 @@ export interface IDirectoryScanner { onBlocksIndexed?: (indexedCount: number) => void, onFileParsed?: (fileBlockCount: number) => void, ): Promise<{ - codeBlocks: CodeBlock[] stats: { processed: number skipped: number diff --git a/src/services/code-index/processors/__tests__/scanner.spec.ts b/src/services/code-index/processors/__tests__/scanner.spec.ts index f90a6c8159..4d4150b443 100644 --- a/src/services/code-index/processors/__tests__/scanner.spec.ts +++ b/src/services/code-index/processors/__tests__/scanner.spec.ts @@ -168,7 +168,16 @@ describe("DirectoryScanner", () => { expect(mockCodeParser.parseFile).not.toHaveBeenCalled() }) - it("should parse changed files and return code blocks", async () => { + it("should parse changed files and return empty codeBlocks array", async () => { + // Create scanner without embedder to test the non-embedding path + const scannerNoEmbeddings = new DirectoryScanner( + null as any, // No embedder + null as any, // No vector store + mockCodeParser, + mockCacheManager, + mockIgnoreInstance, + ) + const { listFiles } = await import("../../../glob/list-files") vi.mocked(listFiles).mockResolvedValue([["test/file1.js"], false]) const mockBlocks: any[] = [ @@ -185,8 +194,7 @@ describe("DirectoryScanner", () => { ] ;(mockCodeParser.parseFile as any).mockResolvedValue(mockBlocks) - const result = await scanner.scanDirectory("/test") - expect(result.codeBlocks).toEqual(mockBlocks) + const result = await scannerNoEmbeddings.scanDirectory("/test") expect(result.stats.processed).toBe(1) }) @@ -252,6 +260,15 @@ describe("DirectoryScanner", () => { }) it("should process markdown files alongside code files", async () => { + // Create scanner without embedder to test the non-embedding path + const scannerNoEmbeddings = new DirectoryScanner( + null as any, // No embedder + null as any, // No vector store + mockCodeParser, + mockCacheManager, + mockIgnoreInstance, + ) + const { listFiles } = await import("../../../glob/list-files") vi.mocked(listFiles).mockResolvedValue([["test/README.md", "test/app.js", "docs/guide.markdown"], false]) @@ -306,7 +323,7 @@ describe("DirectoryScanner", () => { return [] }) - const result = await scanner.scanDirectory("/test") + const result = await scannerNoEmbeddings.scanDirectory("/test") // Verify all files were processed expect(mockCodeParser.parseFile).toHaveBeenCalledTimes(3) @@ -314,16 +331,7 @@ describe("DirectoryScanner", () => { expect(mockCodeParser.parseFile).toHaveBeenCalledWith("test/app.js", expect.any(Object)) expect(mockCodeParser.parseFile).toHaveBeenCalledWith("docs/guide.markdown", expect.any(Object)) - // Verify code blocks include both markdown and code content - expect(result.codeBlocks).toHaveLength(3) - expect(result.codeBlocks).toEqual( - expect.arrayContaining([ - expect.objectContaining({ type: "markdown_header_h1" }), - expect.objectContaining({ type: "function" }), - expect.objectContaining({ type: "markdown_header_h2" }), - ]), - ) - + // Verify processing still works without codeBlocks accumulation expect(result.stats.processed).toBe(3) }) diff --git a/src/services/code-index/processors/scanner.ts b/src/services/code-index/processors/scanner.ts index 538a1252d7..e6ca297399 100644 --- a/src/services/code-index/processors/scanner.ts +++ b/src/services/code-index/processors/scanner.ts @@ -17,7 +17,7 @@ import { t } from "../../../i18n" import { QDRANT_CODE_BLOCK_NAMESPACE, MAX_FILE_SIZE_BYTES, - MAX_LIST_FILES_LIMIT, + MAX_LIST_FILES_LIMIT_CODE_INDEX, BATCH_SEGMENT_THRESHOLD, MAX_BATCH_RETRIES, INITIAL_RETRY_DELAY_MS, @@ -51,13 +51,13 @@ export class DirectoryScanner implements IDirectoryScanner { onError?: (error: Error) => void, onBlocksIndexed?: (indexedCount: number) => void, onFileParsed?: (fileBlockCount: number) => void, - ): Promise<{ codeBlocks: CodeBlock[]; stats: { processed: number; skipped: number }; totalBlockCount: number }> { + ): Promise<{ stats: { processed: number; skipped: number }; totalBlockCount: number }> { const directoryPath = directory // Capture workspace context at scan start const scanWorkspace = getWorkspacePathForContext(directoryPath) // Get all files recursively (handles .gitignore automatically) - const [allPaths, _] = await listFiles(directoryPath, true, MAX_LIST_FILES_LIMIT) + const [allPaths, _] = await listFiles(directoryPath, true, MAX_LIST_FILES_LIMIT_CODE_INDEX) // Filter out directories (marked with trailing '/') const filePaths = allPaths.filter((p) => !p.endsWith("/")) @@ -85,7 +85,6 @@ export class DirectoryScanner implements IDirectoryScanner { // Initialize tracking variables const processedFiles = new Set() - const codeBlocks: CodeBlock[] = [] let processedCount = 0 let skippedCount = 0 @@ -98,7 +97,7 @@ export class DirectoryScanner implements IDirectoryScanner { let currentBatchBlocks: CodeBlock[] = [] let currentBatchTexts: string[] = [] let currentBatchFileInfos: { filePath: string; fileHash: string; isNew: boolean }[] = [] - const activeBatchPromises: Promise[] = [] + const activeBatchPromises = new Set>() // Initialize block counter let totalBlockCount = 0 @@ -125,6 +124,7 @@ export class DirectoryScanner implements IDirectoryScanner { // Check against cache const cachedFileHash = this.cacheManager.getHash(filePath) + const isNewFile = !cachedFileHash if (cachedFileHash === currentFileHash) { // File is unchanged skippedCount++ @@ -135,7 +135,6 @@ export class DirectoryScanner implements IDirectoryScanner { const blocks = await this.codeParser.parseFile(filePath, { content, fileHash: currentFileHash }) const fileBlockCount = blocks.length onFileParsed?.(fileBlockCount) - codeBlocks.push(...blocks) processedCount++ // Process embeddings if configured @@ -146,20 +145,11 @@ export class DirectoryScanner implements IDirectoryScanner { const trimmedContent = block.content.trim() if (trimmedContent) { const release = await mutex.acquire() - totalBlockCount += fileBlockCount try { currentBatchBlocks.push(block) currentBatchTexts.push(trimmedContent) addedBlocksFromFile = true - if (addedBlocksFromFile) { - currentBatchFileInfos.push({ - filePath, - fileHash: currentFileHash, - isNew: !this.cacheManager.getHash(filePath), - }) - } - // Check if batch threshold is met if (currentBatchBlocks.length >= BATCH_SEGMENT_THRESHOLD) { // Copy current batch data and clear accumulators @@ -181,13 +171,33 @@ export class DirectoryScanner implements IDirectoryScanner { onBlocksIndexed, ), ) - activeBatchPromises.push(batchPromise) + activeBatchPromises.add(batchPromise) + + // Clean up completed promises to prevent memory accumulation + batchPromise.finally(() => { + activeBatchPromises.delete(batchPromise) + }) } } finally { release() } } } + + // Add file info once per file (outside the block loop) + if (addedBlocksFromFile) { + const release = await mutex.acquire() + try { + totalBlockCount += fileBlockCount + currentBatchFileInfos.push({ + filePath, + fileHash: currentFileHash, + isNew: isNewFile, + }) + } finally { + release() + } + } } else { // Only update hash if not being processed in a batch await this.cacheManager.updateHash(filePath, currentFileHash) @@ -232,7 +242,12 @@ export class DirectoryScanner implements IDirectoryScanner { const batchPromise = batchLimiter(() => this.processBatch(batchBlocks, batchTexts, batchFileInfos, scanWorkspace, onError, onBlocksIndexed), ) - activeBatchPromises.push(batchPromise) + activeBatchPromises.add(batchPromise) + + // Clean up completed promises to prevent memory accumulation + batchPromise.finally(() => { + activeBatchPromises.delete(batchPromise) + }) } finally { release() } @@ -280,7 +295,6 @@ export class DirectoryScanner implements IDirectoryScanner { } return { - codeBlocks, stats: { processed: processedCount, skipped: skippedCount, From a92ee567df44806908dc67cb18519e9e0e7f7a09 Mon Sep 17 00:00:00 2001 From: Christiaan Arnoldus Date: Wed, 16 Jul 2025 21:58:11 +0200 Subject: [PATCH 7/7] Format time in ISO 8601 (#5793) --- src/core/environment/getEnvironmentDetails.ts | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/core/environment/getEnvironmentDetails.ts b/src/core/environment/getEnvironmentDetails.ts index 8d4f157f4d..6f0c9fe2bf 100644 --- a/src/core/environment/getEnvironmentDetails.ts +++ b/src/core/environment/getEnvironmentDetails.ts @@ -179,22 +179,12 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo // Add current time information with timezone. const now = new Date() - const formatter = new Intl.DateTimeFormat(undefined, { - year: "numeric", - month: "numeric", - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - hour12: true, - }) - - const timeZone = formatter.resolvedOptions().timeZone + const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone const timeZoneOffset = -now.getTimezoneOffset() / 60 // Convert to hours and invert sign to match conventional notation const timeZoneOffsetHours = Math.floor(Math.abs(timeZoneOffset)) const timeZoneOffsetMinutes = Math.abs(Math.round((Math.abs(timeZoneOffset) - timeZoneOffsetHours) * 60)) const timeZoneOffsetStr = `${timeZoneOffset >= 0 ? "+" : "-"}${timeZoneOffsetHours}:${timeZoneOffsetMinutes.toString().padStart(2, "0")}` - details += `\n\n# Current Time\n${formatter.format(now)} (${timeZone}, UTC${timeZoneOffsetStr})` + details += `\n\n# Current Time\nCurrent time in ISO 8601 UTC format: ${now.toISOString()}\nUser time zone: ${timeZone}, UTC${timeZoneOffsetStr}` // Add context tokens information. const { contextTokens, totalCost } = getApiMetrics(cline.clineMessages)