From f6906f6e87c89d4a5a31d3f646167d1cbedc4d69 Mon Sep 17 00:00:00 2001 From: thill2323 Date: Fri, 18 Jul 2025 20:02:35 -0400 Subject: [PATCH 01/27] Added YouTube to website footer (#5929) --- .../src/components/chromes/footer.tsx | 16 ++++++++++++---- apps/web-roo-code/src/lib/constants.ts | 1 + 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/web-roo-code/src/components/chromes/footer.tsx b/apps/web-roo-code/src/components/chromes/footer.tsx index 57d4c8ae8b..4c2b036190 100644 --- a/apps/web-roo-code/src/components/chromes/footer.tsx +++ b/apps/web-roo-code/src/components/chromes/footer.tsx @@ -4,7 +4,7 @@ import { useState, useRef, useEffect } from "react" import Link from "next/link" import Image from "next/image" import { ChevronDown } from "lucide-react" -import { FaBluesky, FaDiscord, FaGithub, FaLinkedin, FaReddit, FaTiktok, FaXTwitter } from "react-icons/fa6" +import { FaBluesky, FaDiscord, FaGithub, FaLinkedin, FaReddit, FaTiktok, FaXTwitter, FaYoutube } from "react-icons/fa6" import { EXTERNAL_LINKS, INTERNAL_LINKS } from "@/lib/constants" import { useLogoSrc } from "@/lib/hooks/use-logo-src" @@ -80,6 +80,14 @@ export function Footer() { LinkedIn + + + Bluesky + TikTok - - Bluesky + + YouTube diff --git a/apps/web-roo-code/src/lib/constants.ts b/apps/web-roo-code/src/lib/constants.ts index 7910d83f2b..9f769e2967 100644 --- a/apps/web-roo-code/src/lib/constants.ts +++ b/apps/web-roo-code/src/lib/constants.ts @@ -6,6 +6,7 @@ export const EXTERNAL_LINKS = { LINKEDIN: "https://www.linkedin.com/company/roo-code", TIKTOK: "https://www.tiktok.com/@roo.code", BLUESKY: "https://bsky.app/profile/roocode.bsky.social", + YOUTUBE: "https://www.youtube.com/@RooCodeYT", DOCUMENTATION: "https://docs.roocode.com", CAREERS: "https://careers.roocode.com", ISSUES: "https://github.com/RooCodeInc/Roo-Code/issues", From 5ed73385c35e61a4a751ead1f04fdbd491e10cbf Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Fri, 18 Jul 2025 20:15:21 -0400 Subject: [PATCH 02/27] chore: add changeset for v3.23.15 patch release (#5934) --- .changeset/v3.23.15.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/v3.23.15.md diff --git a/.changeset/v3.23.15.md b/.changeset/v3.23.15.md new file mode 100644 index 0000000000..d04566a130 --- /dev/null +++ b/.changeset/v3.23.15.md @@ -0,0 +1,11 @@ +--- +"roo-cline": patch +--- + +- Fix configurable delay for diagnostics to prevent premature error reporting +- Add command timeout allowlist +- Add description and whenToUse fields to custom modes in .roomodes (thanks @RandalSchwartz!) +- Fix Claude model detection by name for API protocol selection (thanks @daniel-lxs!) +- Move marketplace icon from overflow menu to top navigation +- Optional setting to prevent completion with open todos +- Added YouTube to website footer (thanks @thill2323!) From 64d35dc35724cdd2c99a3048b804533c436e63fa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 18 Jul 2025 20:21:26 -0400 Subject: [PATCH 03/27] Changeset version bump (#5935) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Matt Rubens --- .changeset/v3.23.15.md | 11 ----------- CHANGELOG.md | 10 ++++++++++ src/package.json | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) delete mode 100644 .changeset/v3.23.15.md diff --git a/.changeset/v3.23.15.md b/.changeset/v3.23.15.md deleted file mode 100644 index d04566a130..0000000000 --- a/.changeset/v3.23.15.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -"roo-cline": patch ---- - -- Fix configurable delay for diagnostics to prevent premature error reporting -- Add command timeout allowlist -- Add description and whenToUse fields to custom modes in .roomodes (thanks @RandalSchwartz!) -- Fix Claude model detection by name for API protocol selection (thanks @daniel-lxs!) -- Move marketplace icon from overflow menu to top navigation -- Optional setting to prevent completion with open todos -- Added YouTube to website footer (thanks @thill2323!) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91091805e8..533de45564 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Roo Code Changelog +## [3.23.15] - 2025-07-18 + +- Fix configurable delay for diagnostics to prevent premature error reporting +- Add command timeout allowlist +- Add description and whenToUse fields to custom modes in .roomodes (thanks @RandalSchwartz!) +- Fix Claude model detection by name for API protocol selection (thanks @daniel-lxs!) +- Move marketplace icon from overflow menu to top navigation +- Optional setting to prevent completion with open todos +- Added YouTube to website footer (thanks @thill2323!) + ## [3.23.14] - 2025-07-17 - Log api-initiated tasks to a tmp directory diff --git a/src/package.json b/src/package.json index 916a109088..b6602e5d8e 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "RooVeterinaryInc", - "version": "3.23.14", + "version": "3.23.15", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", From 5a459d051692c7a9f3f5b5cb7e0a218cceb79955 Mon Sep 17 00:00:00 2001 From: "roomote[bot]" <219738659+roomote[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 00:10:10 -0400 Subject: [PATCH 04/27] Fix Docker port conflicts for evals services (#5909) Co-authored-by: Roo Code Co-authored-by: Matt Rubens --- packages/evals/.env.development | 4 +++- packages/evals/.env.test | 4 +++- packages/evals/README.md | 40 +++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/evals/.env.development b/packages/evals/.env.development index 7970806bec..58f781d246 100644 --- a/packages/evals/.env.development +++ b/packages/evals/.env.development @@ -1 +1,3 @@ -DATABASE_URL=postgres://postgres:password@localhost:5432/evals_development +DATABASE_URL=postgres://postgres:password@localhost:5433/evals_development +EVALS_DB_PORT=5433 +EVALS_REDIS_PORT=6380 diff --git a/packages/evals/.env.test b/packages/evals/.env.test index 2ad09c3734..8ecf74f435 100644 --- a/packages/evals/.env.test +++ b/packages/evals/.env.test @@ -1 +1,3 @@ -DATABASE_URL=postgres://postgres:password@localhost:5432/evals_test +DATABASE_URL=postgres://postgres:password@localhost:5433/evals_test +EVALS_DB_PORT=5433 +EVALS_REDIS_PORT=6380 diff --git a/packages/evals/README.md b/packages/evals/README.md index 7ef5be139b..750454956f 100644 --- a/packages/evals/README.md +++ b/packages/evals/README.md @@ -89,6 +89,46 @@ The setup script does the following: - Prompts for an OpenRouter API key to add to `.env.local` - Optionally builds and installs the Roo Code extension from source +## Port Configuration + +By default, the evals system uses the following ports: + +- **PostgreSQL**: 5433 (external) → 5432 (internal) +- **Redis**: 6380 (external) → 6379 (internal) +- **Web Service**: 3446 (external) → 3000 (internal) + +These ports are configured to avoid conflicts with other services that might be running on the standard PostgreSQL (5432) and Redis (6379) ports. + +### Customizing Ports + +If you need to use different ports, you can customize them by creating a `.env.local` file in the `packages/evals/` directory: + +```sh +# Copy the example file and customize as needed +cp packages/evals/.env.local.example packages/evals/.env.local +``` + +Then edit `.env.local` to set your preferred ports: + +```sh +# Custom port configuration +EVALS_DB_PORT=5434 +EVALS_REDIS_PORT=6381 +EVALS_WEB_PORT=3447 + +# Optional: Override database URL if needed +DATABASE_URL=postgres://postgres:password@localhost:5434/evals_development +``` + +### Port Conflict Resolution + +If you encounter port conflicts when running `pnpm evals`, you have several options: + +1. **Use the default configuration** (recommended): The system now uses non-standard ports by default +2. **Stop conflicting services**: Temporarily stop other PostgreSQL/Redis services +3. **Customize ports**: Use the `.env.local` file to set different ports +4. **Use Docker networks**: Run services in isolated Docker networks + ## Troubleshooting Here are some errors that you might encounter along with potential fixes: From e28fad131a84b681fa84a1919c1decf846c4b747 Mon Sep 17 00:00:00 2001 From: "roomote[bot]" <219738659+roomote[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 01:07:27 -0400 Subject: [PATCH 05/27] feat: mark non-English translation files as linguist-generated (#5943) Co-authored-by: Roo Code --- .gitattributes | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.gitattributes b/.gitattributes index 02ddd6b634..284eab4f98 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,3 +4,17 @@ src/assets/docs/demo.gif filter=lfs diff=lfs merge=lfs -text # Test snapshot files - mark as linguist-generated to exclude from GitHub language statistics *.snap linguist-generated=true + +# Non-English translation files - mark as linguist-generated to exclude from GitHub language statistics +# Root locales directory (contains only non-English translations) +locales/** linguist-generated=true + +# Mark all locale directories as generated first +src/i18n/locales/** linguist-generated=true +webview-ui/src/i18n/locales/** linguist-generated=true + +# Then explicitly mark English directories as NOT generated (override the above) +src/i18n/locales/en/** linguist-generated=false +webview-ui/src/i18n/locales/en/** linguist-generated=false + +# This approach uses gitattributes' last-match-wins rule to exclude English while including all other locales From 020c2337dd389bac4822807909fe735a4b6e4a67 Mon Sep 17 00:00:00 2001 From: Daniel <57051444+daniel-lxs@users.noreply.github.com> Date: Sat, 19 Jul 2025 08:37:01 -0500 Subject: [PATCH 06/27] feat: add global rate limiting for OpenAI-compatible embeddings (#5854) * feat: add global rate limiting for OpenAI-compatible embeddings - Implement shared rate limit state across all embedder instances - Add exponential backoff (5s base, up to 5 minutes max) - Track consecutive rate limit errors with auto-reset after 60s - Add thread-safe mutex for concurrent access - Remove verbose logging to prevent log flooding during retries - Add comprehensive test coverage for rate limiting behavior This prevents multiple parallel batches from overwhelming APIs with restrictive rate limits by coordinating delays globally. * fix: restore original logging while keeping rate limit logging silent - Restored all original console.warn and console.error calls that were inadvertently removed - Only the rate limit retry warning remains silent (no console output) - Fixed test expectation to use correct error variable - All existing logging behavior is preserved --- .../openai-compatible-rate-limit.spec.ts | 213 ++++++++++++++++++ .../__tests__/openai-compatible.spec.ts | 43 +++- .../code-index/embedders/openai-compatible.ts | 128 ++++++++++- 3 files changed, 367 insertions(+), 17 deletions(-) create mode 100644 src/services/code-index/embedders/__tests__/openai-compatible-rate-limit.spec.ts diff --git a/src/services/code-index/embedders/__tests__/openai-compatible-rate-limit.spec.ts b/src/services/code-index/embedders/__tests__/openai-compatible-rate-limit.spec.ts new file mode 100644 index 0000000000..3e2acc398e --- /dev/null +++ b/src/services/code-index/embedders/__tests__/openai-compatible-rate-limit.spec.ts @@ -0,0 +1,213 @@ +import { describe, it, expect, vi, beforeEach, afterEach, MockedClass, MockedFunction } from "vitest" +import { OpenAI } from "openai" +import { OpenAICompatibleEmbedder } from "../openai-compatible" + +// Mock the OpenAI SDK +vi.mock("openai") + +// Mock TelemetryService +vi.mock("@roo-code/telemetry", () => ({ + TelemetryService: { + instance: { + captureEvent: vi.fn(), + }, + }, +})) + +// Mock i18n +vi.mock("../../../../i18n", () => ({ + t: (key: string, params?: Record) => { + const translations: Record = { + "embeddings:rateLimitRetry": `Rate limit hit, retrying in ${params?.delayMs}ms (attempt ${params?.attempt}/${params?.maxRetries})`, + "embeddings:failedMaxAttempts": `Failed to create embeddings after ${params?.attempts} attempts`, + "embeddings:failedWithStatus": `Failed to create embeddings after ${params?.attempts} attempts: HTTP ${params?.statusCode} - ${params?.errorMessage}`, + "embeddings:failedWithError": `Failed to create embeddings after ${params?.attempts} attempts: ${params?.errorMessage}`, + } + return translations[key] || key + }, +})) + +const MockedOpenAI = OpenAI as MockedClass + +describe("OpenAICompatibleEmbedder - Global Rate Limiting", () => { + let mockOpenAIInstance: any + let mockEmbeddingsCreate: MockedFunction + + const testBaseUrl = "https://api.openai.com/v1" + const testApiKey = "test-api-key" + const testModelId = "text-embedding-3-small" + + beforeEach(() => { + vi.clearAllMocks() + vi.useFakeTimers() + vi.spyOn(console, "warn").mockImplementation(() => {}) + vi.spyOn(console, "error").mockImplementation(() => {}) + + // Setup mock OpenAI instance + mockEmbeddingsCreate = vi.fn() + mockOpenAIInstance = { + embeddings: { + create: mockEmbeddingsCreate, + }, + } + + MockedOpenAI.mockImplementation(() => mockOpenAIInstance) + + // Reset global rate limit state + const embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId) + ;(embedder as any).constructor.globalRateLimitState = { + isRateLimited: false, + rateLimitResetTime: 0, + consecutiveRateLimitErrors: 0, + lastRateLimitError: 0, + mutex: (embedder as any).constructor.globalRateLimitState.mutex, + } + }) + + afterEach(() => { + vi.useRealTimers() + vi.restoreAllMocks() + }) + + it("should apply global rate limiting across multiple batch requests", async () => { + const embedder1 = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId) + const embedder2 = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId) + + // First batch hits rate limit + const rateLimitError = new Error("Rate limit exceeded") as any + rateLimitError.status = 429 + + mockEmbeddingsCreate + .mockRejectedValueOnce(rateLimitError) // First attempt fails + .mockResolvedValue({ + data: [{ embedding: "base64encodeddata" }], + usage: { prompt_tokens: 10, total_tokens: 15 }, + }) + + // Start first batch request + const batch1Promise = embedder1.createEmbeddings(["test1"]) + + // Advance time slightly to let the first request fail and set global rate limit + await vi.advanceTimersByTimeAsync(100) + + // Start second batch request while global rate limit is active + const batch2Promise = embedder2.createEmbeddings(["test2"]) + + // Check that global rate limit was set + const state = (embedder1 as any).constructor.globalRateLimitState + expect(state.isRateLimited).toBe(true) + expect(state.consecutiveRateLimitErrors).toBe(1) + + // Advance time to complete rate limit delay (5 seconds base delay) + await vi.advanceTimersByTimeAsync(5000) + + // Both requests should complete + const [result1, result2] = await Promise.all([batch1Promise, batch2Promise]) + + expect(result1.embeddings).toHaveLength(1) + expect(result2.embeddings).toHaveLength(1) + + // The second embedder should have waited for the global rate limit + // No logging expected - we've removed it to prevent log flooding + }) + + it("should track consecutive rate limit errors", async () => { + const embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId) + const state = (embedder as any).constructor.globalRateLimitState + + const rateLimitError = new Error("Rate limit exceeded") as any + rateLimitError.status = 429 + + // Test that consecutive errors increment when they happen quickly + // Mock multiple rate limit errors in a single request + mockEmbeddingsCreate + .mockRejectedValueOnce(rateLimitError) // First attempt + .mockRejectedValueOnce(rateLimitError) // Retry 1 + .mockResolvedValueOnce({ + data: [{ embedding: "base64encodeddata" }], + usage: { prompt_tokens: 10, total_tokens: 15 }, + }) + + const promise1 = embedder.createEmbeddings(["test1"]) + + // Wait for first attempt to fail + await vi.advanceTimersByTimeAsync(100) + expect(state.consecutiveRateLimitErrors).toBe(1) + + // Wait for first retry (500ms) to also fail + await vi.advanceTimersByTimeAsync(500) + + // The state should show 2 consecutive errors now + // Note: The count might be 1 if the global rate limit kicked in before the second attempt + expect(state.consecutiveRateLimitErrors).toBeGreaterThanOrEqual(1) + + // Wait for the global rate limit and successful retry + await vi.advanceTimersByTimeAsync(20000) + await promise1 + + // Verify the delay increases with consecutive errors + // Make another request immediately that also hits rate limit + mockEmbeddingsCreate.mockRejectedValueOnce(rateLimitError).mockResolvedValueOnce({ + data: [{ embedding: "base64encodeddata" }], + usage: { prompt_tokens: 10, total_tokens: 15 }, + }) + + // Store the current consecutive count before the next request + const previousCount = state.consecutiveRateLimitErrors + + const promise2 = embedder.createEmbeddings(["test2"]) + await vi.advanceTimersByTimeAsync(100) + + // Should have incremented from the previous count + expect(state.consecutiveRateLimitErrors).toBeGreaterThan(previousCount) + + // Complete the second request + await vi.advanceTimersByTimeAsync(20000) + await promise2 + }) + + it("should reset consecutive error count after time passes", async () => { + const embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId) + const state = (embedder as any).constructor.globalRateLimitState + + // Manually set state to simulate previous errors + state.consecutiveRateLimitErrors = 3 + state.lastRateLimitError = Date.now() - 70000 // 70 seconds ago + + const rateLimitError = new Error("Rate limit exceeded") as any + rateLimitError.status = 429 + + mockEmbeddingsCreate.mockRejectedValueOnce(rateLimitError).mockResolvedValueOnce({ + data: [{ embedding: "base64encodeddata" }], + usage: { prompt_tokens: 10, total_tokens: 15 }, + }) + + // Trigger the updateGlobalRateLimitState method + await (embedder as any).updateGlobalRateLimitState(rateLimitError) + + // Should reset to 1 since more than 60 seconds passed + expect(state.consecutiveRateLimitErrors).toBe(1) + }) + + it("should not exceed maximum delay of 5 minutes", async () => { + const embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId) + const state = (embedder as any).constructor.globalRateLimitState + + // Set state to simulate many consecutive errors + state.consecutiveRateLimitErrors = 10 // This would normally result in a very long delay + + const rateLimitError = new Error("Rate limit exceeded") as any + rateLimitError.status = 429 + + // Trigger the updateGlobalRateLimitState method + await (embedder as any).updateGlobalRateLimitState(rateLimitError) + + // Calculate the expected delay + const now = Date.now() + const delay = state.rateLimitResetTime - now + + // Should be capped at 5 minutes (300000ms) + expect(delay).toBeLessThanOrEqual(300000) + expect(delay).toBeGreaterThan(0) + }) +}) diff --git a/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts b/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts index ff757b86c7..0353771f60 100644 --- a/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts +++ b/src/services/code-index/embedders/__tests__/openai-compatible.spec.ts @@ -60,6 +60,16 @@ describe("OpenAICompatibleEmbedder", () => { } MockedOpenAI.mockImplementation(() => mockOpenAIInstance) + + // Reset global rate limit state to prevent interference between tests + const tempEmbedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId) + ;(tempEmbedder as any).constructor.globalRateLimitState = { + isRateLimited: false, + rateLimitResetTime: 0, + consecutiveRateLimitErrors: 0, + lastRateLimitError: 0, + mutex: (tempEmbedder as any).constructor.globalRateLimitState.mutex, + } }) afterEach(() => { @@ -385,9 +395,17 @@ describe("OpenAICompatibleEmbedder", () => { const resultPromise = embedder.createEmbeddings(testTexts) - // Fast-forward through the delays - await vitest.advanceTimersByTimeAsync(INITIAL_RETRY_DELAY_MS) // First retry delay - await vitest.advanceTimersByTimeAsync(INITIAL_RETRY_DELAY_MS * 2) // Second retry delay + // First attempt fails immediately, triggering global rate limit (5s) + await vitest.advanceTimersByTimeAsync(100) + + // Wait for global rate limit delay + await vitest.advanceTimersByTimeAsync(5000) + + // Second attempt also fails, increasing delay + await vitest.advanceTimersByTimeAsync(100) + + // Wait for increased global rate limit delay (10s) + await vitest.advanceTimersByTimeAsync(10000) const result = await resultPromise @@ -445,7 +463,7 @@ describe("OpenAICompatibleEmbedder", () => { expect(console.error).toHaveBeenCalledWith( expect.stringContaining("OpenAI Compatible embedder error"), - expect.any(Error), + apiError, ) }) @@ -461,7 +479,7 @@ describe("OpenAICompatibleEmbedder", () => { expect(console.error).toHaveBeenCalledWith( expect.stringContaining("OpenAI Compatible embedder error"), - batchError, + expect.any(Error), ) }) @@ -791,10 +809,23 @@ describe("OpenAICompatibleEmbedder", () => { ) const resultPromise = embedder.createEmbeddings(["test"]) - await vitest.advanceTimersByTimeAsync(INITIAL_RETRY_DELAY_MS * 3) + + // First attempt fails, triggering global rate limit + await vitest.advanceTimersByTimeAsync(100) + + // Wait for global rate limit (5s) + await vitest.advanceTimersByTimeAsync(5000) + + // Second attempt also fails + await vitest.advanceTimersByTimeAsync(100) + + // Wait for increased global rate limit (10s) + await vitest.advanceTimersByTimeAsync(10000) + const result = await resultPromise expect(global.fetch).toHaveBeenCalledTimes(3) + // Check that rate limit warnings were logged expect(console.warn).toHaveBeenCalledWith(expect.stringContaining("Rate limit hit")) expectEmbeddingValues(result.embeddings[0], [0.1, 0.2, 0.3]) vitest.useRealTimers() diff --git a/src/services/code-index/embedders/openai-compatible.ts b/src/services/code-index/embedders/openai-compatible.ts index d882e78313..035f50f386 100644 --- a/src/services/code-index/embedders/openai-compatible.ts +++ b/src/services/code-index/embedders/openai-compatible.ts @@ -11,6 +11,7 @@ import { t } from "../../../i18n" import { withValidationErrorHandling, HttpError, formatEmbeddingError } from "../shared/validation-helpers" import { TelemetryEventName } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" +import { Mutex } from "async-mutex" interface EmbeddingItem { embedding: string | number[] @@ -38,6 +39,16 @@ export class OpenAICompatibleEmbedder implements IEmbedder { private readonly isFullUrl: boolean private readonly maxItemTokens: number + // Global rate limiting state shared across all instances + private static globalRateLimitState = { + isRateLimited: false, + rateLimitResetTime: 0, + consecutiveRateLimitErrors: 0, + lastRateLimitError: 0, + // Mutex to ensure thread-safe access to rate limit state + mutex: new Mutex(), + } + /** * Creates a new OpenAI Compatible embedder * @param baseUrl The base URL for the OpenAI-compatible API endpoint @@ -239,6 +250,9 @@ export class OpenAICompatibleEmbedder implements IEmbedder { const isFullUrl = this.isFullUrl for (let attempts = 0; attempts < MAX_RETRIES; attempts++) { + // Check global rate limit before attempting request + await this.waitForGlobalRateLimit() + try { let response: OpenAIEmbeddingResponse @@ -298,17 +312,26 @@ export class OpenAICompatibleEmbedder implements IEmbedder { // Check if it's a rate limit error const httpError = error as HttpError - if (httpError?.status === 429 && hasMoreAttempts) { - const delayMs = INITIAL_DELAY_MS * Math.pow(2, attempts) - console.warn( - t("embeddings:rateLimitRetry", { - delayMs, - attempt: attempts + 1, - maxRetries: MAX_RETRIES, - }), - ) - await new Promise((resolve) => setTimeout(resolve, delayMs)) - continue + if (httpError?.status === 429) { + // Update global rate limit state + await this.updateGlobalRateLimitState(httpError) + + if (hasMoreAttempts) { + // Calculate delay based on global rate limit state + const baseDelay = INITIAL_DELAY_MS * Math.pow(2, attempts) + const globalDelay = await this.getGlobalRateLimitDelay() + const delayMs = Math.max(baseDelay, globalDelay) + + console.warn( + t("embeddings:rateLimitRetry", { + delayMs, + attempt: attempts + 1, + maxRetries: MAX_RETRIES, + }), + ) + await new Promise((resolve) => setTimeout(resolve, delayMs)) + continue + } } // Log the error for debugging @@ -376,4 +399,87 @@ export class OpenAICompatibleEmbedder implements IEmbedder { name: "openai-compatible", } } + + /** + * Waits if there's an active global rate limit + */ + private async waitForGlobalRateLimit(): Promise { + const release = await OpenAICompatibleEmbedder.globalRateLimitState.mutex.acquire() + try { + const state = OpenAICompatibleEmbedder.globalRateLimitState + + if (state.isRateLimited && state.rateLimitResetTime > Date.now()) { + const waitTime = state.rateLimitResetTime - Date.now() + // Silent wait - no logging to prevent flooding + release() // Release mutex before waiting + await new Promise((resolve) => setTimeout(resolve, waitTime)) + return + } + + // Reset rate limit if time has passed + if (state.isRateLimited && state.rateLimitResetTime <= Date.now()) { + state.isRateLimited = false + state.consecutiveRateLimitErrors = 0 + } + } finally { + // Only release if we haven't already + try { + release() + } catch { + // Already released + } + } + } + + /** + * Updates global rate limit state when a 429 error occurs + */ + private async updateGlobalRateLimitState(error: HttpError): Promise { + const release = await OpenAICompatibleEmbedder.globalRateLimitState.mutex.acquire() + try { + const state = OpenAICompatibleEmbedder.globalRateLimitState + const now = Date.now() + + // Increment consecutive rate limit errors + if (now - state.lastRateLimitError < 60000) { + // Within 1 minute + state.consecutiveRateLimitErrors++ + } else { + state.consecutiveRateLimitErrors = 1 + } + + state.lastRateLimitError = now + + // Calculate exponential backoff based on consecutive errors + const baseDelay = 5000 // 5 seconds base + const maxDelay = 300000 // 5 minutes max + const exponentialDelay = Math.min(baseDelay * Math.pow(2, state.consecutiveRateLimitErrors - 1), maxDelay) + + // Set global rate limit + state.isRateLimited = true + state.rateLimitResetTime = now + exponentialDelay + + // Silent rate limit activation - no logging to prevent flooding + } finally { + release() + } + } + + /** + * Gets the current global rate limit delay + */ + private async getGlobalRateLimitDelay(): Promise { + const release = await OpenAICompatibleEmbedder.globalRateLimitState.mutex.acquire() + try { + const state = OpenAICompatibleEmbedder.globalRateLimitState + + if (state.isRateLimited && state.rateLimitResetTime > Date.now()) { + return state.rateLimitResetTime - Date.now() + } + + return 0 + } finally { + release() + } + } } From cc369da2d5567e2284d715370cd0751cc55958dc Mon Sep 17 00:00:00 2001 From: Daniel <57051444+daniel-lxs@users.noreply.github.com> Date: Sat, 19 Jul 2025 08:37:37 -0500 Subject: [PATCH 07/27] feat: add batch limiting to code indexer (#5891) feat: add batch limiting to code indexer to control memory usage - Add MAX_PENDING_BATCHES constant (20) to limit concurrent batches - Implement backpressure mechanism to pause file parsing when limit reached - Prevent memory overflow during large codebase indexing --- src/services/code-index/constants/index.ts | 1 + src/services/code-index/processors/scanner.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/services/code-index/constants/index.ts b/src/services/code-index/constants/index.ts index 706a73935a..6f0e0fe7e6 100644 --- a/src/services/code-index/constants/index.ts +++ b/src/services/code-index/constants/index.ts @@ -20,6 +20,7 @@ export const BATCH_SEGMENT_THRESHOLD = 60 // Number of code segments to batch fo export const MAX_BATCH_RETRIES = 3 export const INITIAL_RETRY_DELAY_MS = 500 export const PARSING_CONCURRENCY = 10 +export const MAX_PENDING_BATCHES = 20 // Maximum number of batches to accumulate before waiting /**OpenAI Embedder */ export const MAX_BATCH_TOKENS = 100000 diff --git a/src/services/code-index/processors/scanner.ts b/src/services/code-index/processors/scanner.ts index e6ca297399..3203076d12 100644 --- a/src/services/code-index/processors/scanner.ts +++ b/src/services/code-index/processors/scanner.ts @@ -23,6 +23,7 @@ import { INITIAL_RETRY_DELAY_MS, PARSING_CONCURRENCY, BATCH_PROCESSING_CONCURRENCY, + MAX_PENDING_BATCHES, } from "../constants" import { isPathInIgnoredDirectory } from "../../glob/ignore-utils" import { TelemetryService } from "@roo-code/telemetry" @@ -98,6 +99,7 @@ export class DirectoryScanner implements IDirectoryScanner { let currentBatchTexts: string[] = [] let currentBatchFileInfos: { filePath: string; fileHash: string; isNew: boolean }[] = [] const activeBatchPromises = new Set>() + let pendingBatchCount = 0 // Initialize block counter let totalBlockCount = 0 @@ -152,6 +154,12 @@ export class DirectoryScanner implements IDirectoryScanner { // Check if batch threshold is met if (currentBatchBlocks.length >= BATCH_SEGMENT_THRESHOLD) { + // Wait if we've reached the maximum pending batches + while (pendingBatchCount >= MAX_PENDING_BATCHES) { + // Wait for at least one batch to complete + await Promise.race(activeBatchPromises) + } + // Copy current batch data and clear accumulators const batchBlocks = [...currentBatchBlocks] const batchTexts = [...currentBatchTexts] @@ -160,6 +168,9 @@ export class DirectoryScanner implements IDirectoryScanner { currentBatchTexts = [] currentBatchFileInfos = [] + // Increment pending batch count + pendingBatchCount++ + // Queue batch processing const batchPromise = batchLimiter(() => this.processBatch( @@ -176,6 +187,7 @@ export class DirectoryScanner implements IDirectoryScanner { // Clean up completed promises to prevent memory accumulation batchPromise.finally(() => { activeBatchPromises.delete(batchPromise) + pendingBatchCount-- }) } } finally { @@ -238,6 +250,9 @@ export class DirectoryScanner implements IDirectoryScanner { currentBatchTexts = [] currentBatchFileInfos = [] + // Increment pending batch count for final batch + pendingBatchCount++ + // Queue final batch processing const batchPromise = batchLimiter(() => this.processBatch(batchBlocks, batchTexts, batchFileInfos, scanWorkspace, onError, onBlocksIndexed), @@ -247,6 +262,7 @@ export class DirectoryScanner implements IDirectoryScanner { // Clean up completed promises to prevent memory accumulation batchPromise.finally(() => { activeBatchPromises.delete(batchPromise) + pendingBatchCount-- }) } finally { release() From 539b716b4f24628e5eb0e32bdfff0a945842eabe Mon Sep 17 00:00:00 2001 From: "roomote[bot]" <219738659+roomote[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 11:42:59 -0400 Subject: [PATCH 08/27] feat: enhance release engineer mode to include issue numbers and reporters in changelog (#5940) Co-authored-by: Roo Code --- .roomodes | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.roomodes b/.roomodes index 15d3673a09..cf066e888b 100644 --- a/.roomodes +++ b/.roomodes @@ -75,11 +75,14 @@ customModes: whenToUse: Automate the release process for software projects. description: Automate the release process. customInstructions: |- - When preparing a release: 1. Identify the SHA corresponding to the most recent release using GitHub CLI: `gh release view --json tagName,targetCommitish,publishedAt ` 2. Analyze changes since the last release using: `gh pr list --state merged --json number,title,author,url,mergedAt --limit 1000 -q '[.[] | select(.mergedAt > "TIMESTAMP") | {number, title, author: .author.login, url, mergedAt}] | sort_by(.number)'` 3. Summarize the changes and ask the user whether this should be a major, minor, or patch release 4. Create a changeset in .changeset/v[version].md instead of directly modifying package.json. The format is: + When preparing a release: 1. Identify the SHA corresponding to the most recent release using GitHub CLI: `gh release view --json tagName,targetCommitish,publishedAt ` 2. Analyze changes since the last release using: `gh pr list --state merged --json number,title,author,url,mergedAt,closingIssuesReferences --limit 1000 -q '[.[] | select(.mergedAt > "TIMESTAMP") | {number, title, author: .author.login, url, mergedAt, issues: .closingIssuesReferences}] | sort_by(.number)'` 3. For each PR with linked issues, fetch the issue details to get the issue reporter: `gh issue view ISSUE_NUMBER --json number,author -q '{number, reporter: .author.login}'` 4. Summarize the changes and ask the user whether this should be a major, minor, or patch release 5. Create a changeset in .changeset/v[version].md instead of directly modifying package.json. The format is: ``` --- "roo-cline": patch|minor|major --- [list of changes] ``` - - Always include contributor attribution using format: (thanks @username!) - Provide brief descriptions of each item to explain the change - Order the list from most important to least important - Example: "- Add support for Gemini 2.5 Pro caching (thanks @contributor!)" - CRITICAL: Include EVERY SINGLE PR in the changeset - don't assume you know which ones are important. Count the total PRs to verify completeness and cross-reference the list to ensure nothing is missed. - 5. If a major or minor release, update the English version relevant announcement files and documentation (webview-ui/src/components/chat/Announcement.tsx, README.md, and the `latestAnnouncementId` in src/core/webview/ClineProvider.ts) 6. Ask the user to confirm the English version 7. Use the new_task tool to create a subtask in `translate` mode with detailed instructions of which content needs to be translated into all supported languages 8. Commit and push the changeset file to the repository 9. The GitHub Actions workflow will automatically: + - Always include contributor attribution using format: (thanks @username!) - For PRs that close issues, also include the issue number and reporter: "- Fix: Description (#123 by @reporter, PR by @contributor)" - For PRs without linked issues, use the standard format: "- Add support for feature (thanks @contributor!)" - Provide brief descriptions of each item to explain the change - Order the list from most important to least important - Example formats: + - With issue: "- Fix: Resolve memory leak in extension (#456 by @issueReporter, PR by @prAuthor)" + - Without issue: "- Add support for Gemini 2.5 Pro caching (thanks @contributor!)" + - CRITICAL: Include EVERY SINGLE PR in the changeset - don't assume you know which ones are important. Count the total PRs to verify completeness and cross-reference the list to ensure nothing is missed. + 6. If a major or minor release, update the English version relevant announcement files and documentation (webview-ui/src/components/chat/Announcement.tsx, README.md, and the `latestAnnouncementId` in src/core/webview/ClineProvider.ts) 7. Ask the user to confirm the English version 8. Use the new_task tool to create a subtask in `translate` mode with detailed instructions of which content needs to be translated into all supported languages 9. Commit and push the changeset file to the repository 10. The GitHub Actions workflow will automatically: - Create a version bump PR when changesets are merged to main - Update the CHANGELOG.md with proper formatting - Publish the release when the version bump PR is merged From fe3a7ab0f059710fc288b16e54d2c67c789db823 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sat, 19 Jul 2025 11:55:18 -0400 Subject: [PATCH 09/27] Release v3.23.16 (#5952) --- .changeset/v3.23.16.md | 7 +++++++ .roomodes | 29 +++++++++++++++++++++-------- 2 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 .changeset/v3.23.16.md diff --git a/.changeset/v3.23.16.md b/.changeset/v3.23.16.md new file mode 100644 index 0000000000..dd1f02351f --- /dev/null +++ b/.changeset/v3.23.16.md @@ -0,0 +1,7 @@ +--- +"roo-cline": patch +--- + +- Add global rate limiting for OpenAI-compatible embeddings (thanks @daniel-lxs!) +- Add batch limiting to code indexer (thanks @daniel-lxs!) +- Fix Docker port conflicts for evals services diff --git a/.roomodes b/.roomodes index cf066e888b..d98a0fda7d 100644 --- a/.roomodes +++ b/.roomodes @@ -75,14 +75,27 @@ customModes: whenToUse: Automate the release process for software projects. description: Automate the release process. customInstructions: |- - When preparing a release: 1. Identify the SHA corresponding to the most recent release using GitHub CLI: `gh release view --json tagName,targetCommitish,publishedAt ` 2. Analyze changes since the last release using: `gh pr list --state merged --json number,title,author,url,mergedAt,closingIssuesReferences --limit 1000 -q '[.[] | select(.mergedAt > "TIMESTAMP") | {number, title, author: .author.login, url, mergedAt, issues: .closingIssuesReferences}] | sort_by(.number)'` 3. For each PR with linked issues, fetch the issue details to get the issue reporter: `gh issue view ISSUE_NUMBER --json number,author -q '{number, reporter: .author.login}'` 4. Summarize the changes and ask the user whether this should be a major, minor, or patch release 5. Create a changeset in .changeset/v[version].md instead of directly modifying package.json. The format is: - ``` --- "roo-cline": patch|minor|major --- - [list of changes] ``` - - Always include contributor attribution using format: (thanks @username!) - For PRs that close issues, also include the issue number and reporter: "- Fix: Description (#123 by @reporter, PR by @contributor)" - For PRs without linked issues, use the standard format: "- Add support for feature (thanks @contributor!)" - Provide brief descriptions of each item to explain the change - Order the list from most important to least important - Example formats: - - With issue: "- Fix: Resolve memory leak in extension (#456 by @issueReporter, PR by @prAuthor)" - - Without issue: "- Add support for Gemini 2.5 Pro caching (thanks @contributor!)" - - CRITICAL: Include EVERY SINGLE PR in the changeset - don't assume you know which ones are important. Count the total PRs to verify completeness and cross-reference the list to ensure nothing is missed. - 6. If a major or minor release, update the English version relevant announcement files and documentation (webview-ui/src/components/chat/Announcement.tsx, README.md, and the `latestAnnouncementId` in src/core/webview/ClineProvider.ts) 7. Ask the user to confirm the English version 8. Use the new_task tool to create a subtask in `translate` mode with detailed instructions of which content needs to be translated into all supported languages 9. Commit and push the changeset file to the repository 10. The GitHub Actions workflow will automatically: + When preparing a release: + 1. Identify the SHA corresponding to the most recent release using GitHub CLI: `gh release view --json tagName,targetCommitish,publishedAt` + 2. Analyze changes since the last release using: `gh pr list --state merged --json number,title,author,url,mergedAt,closingIssuesReferences --limit 1000 -q '[.[] | select(.mergedAt > "TIMESTAMP") | {number, title, author: .author.login, url, mergedAt, issues: .closingIssuesReferences}] | sort_by(.number)'` + 3. For each PR with linked issues, fetch the issue details to get the issue reporter: `gh issue view ISSUE_NUMBER --json number,author -q '{number, reporter: .author.login}'` + 4. Summarize the changes and ask the user whether this should be a major, minor, or patch release + 5. Create a changeset in .changeset/v[version].md instead of directly modifying package.json. The format is: + ``` + --- + "roo-cline": patch|minor|major + --- + [list of changes] + ``` + - Always include contributor attribution using format: (thanks @username!) - For PRs that close issues, also include the issue number and reporter: "- Fix: Description (#123 by @reporter, PR by @contributor)" - For PRs without linked issues, use the standard format: "- Add support for feature (thanks @contributor!)" - Provide brief descriptions of each item to explain the change - Order the list from most important to least important - Example formats: + - With issue: "- Fix: Resolve memory leak in extension (#456 by @issueReporter, PR by @prAuthor)" + - Without issue: "- Add support for Gemini 2.5 Pro caching (thanks @contributor!)" + - CRITICAL: Include EVERY SINGLE PR in the changeset - don't assume you know which ones are important. Count the total PRs to verify completeness and cross-reference the list to ensure nothing is missed. + 6. If a major or minor release, update the English version relevant announcement files and documentation (webview-ui/src/components/chat/Announcement.tsx, README.md, and the `latestAnnouncementId` in src/core/webview/ClineProvider.ts) + 7. Ask the user to confirm the English version + 8. Use the new_task tool to create a subtask in `translate` mode with detailed instructions of which content needs to be translated into all supported languages + 9. Create a new branch for the release preparation: `git checkout -b release/v[version]` + 10. Commit and push the changeset file and any documentation updates to the repository: `git add . && git commit -m "chore: add changeset for v[version]" && git push origin release/v[version]` 11. Create a pull request for the release: `gh pr create --title "Release v[version]" --body "Release preparation for v[version]. This PR includes the changeset and any necessary documentation updates." --base main --head release/v[version]` 12. The GitHub Actions workflow will automatically: - Create a version bump PR when changesets are merged to main - Update the CHANGELOG.md with proper formatting - Publish the release when the version bump PR is merged From e2970265dd29a7cb0dc260eeae5d57e3c5c443a0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 11:58:06 -0400 Subject: [PATCH 10/27] Changeset version bump (#5953) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Matt Rubens --- .changeset/v3.23.16.md | 7 ------- CHANGELOG.md | 6 ++++++ src/package.json | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) delete mode 100644 .changeset/v3.23.16.md diff --git a/.changeset/v3.23.16.md b/.changeset/v3.23.16.md deleted file mode 100644 index dd1f02351f..0000000000 --- a/.changeset/v3.23.16.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"roo-cline": patch ---- - -- Add global rate limiting for OpenAI-compatible embeddings (thanks @daniel-lxs!) -- Add batch limiting to code indexer (thanks @daniel-lxs!) -- Fix Docker port conflicts for evals services diff --git a/CHANGELOG.md b/CHANGELOG.md index 533de45564..7eca706181 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Roo Code Changelog +## [3.23.16] - 2025-07-19 + +- Add global rate limiting for OpenAI-compatible embeddings (thanks @daniel-lxs!) +- Add batch limiting to code indexer (thanks @daniel-lxs!) +- Fix Docker port conflicts for evals services + ## [3.23.15] - 2025-07-18 - Fix configurable delay for diagnostics to prevent premature error reporting diff --git a/src/package.json b/src/package.json index b6602e5d8e..5e3cd3bc53 100644 --- a/src/package.json +++ b/src/package.json @@ -3,7 +3,7 @@ "displayName": "%extension.displayName%", "description": "%extension.description%", "publisher": "RooVeterinaryInc", - "version": "3.23.15", + "version": "3.23.16", "icon": "assets/icons/icon.png", "galleryBanner": { "color": "#617A91", From 7ddc4e64f3c63971a8c1de6f57344b271f0ad527 Mon Sep 17 00:00:00 2001 From: SannidhyaSah Date: Sat, 19 Jul 2025 22:54:24 +0530 Subject: [PATCH 11/27] feat: Add Mistral embedding provider (#5932) (#5946) * feat: add Mistral embedding provider with OpenAI Compatible Wrapper - Implement MistralEmbedder class using OpenAI-compatible API - Add comprehensive unit tests with 100% coverage - Update type definitions for Mistral provider support - Integrate Mistral option in UI components and configuration - Add internationalization support for Mistral provider - Fix API key storage and retrieval for embedding providers - Update service factory to support Mistral embeddings - Add proper error handling and validation This implementation allows users to use Mistral's embedding models through the existing OpenAI-compatible wrapper approach, providing a seamless integration experience. * feat: add Mistral embedding provider support - Implement MistralEmbedder class with API integration - Add Mistral models to embedding model configurations - Update UI to include Mistral provider option - Add comprehensive unit tests for Mistral embedder - Update type definitions and interfaces - Add internationalization support for Mistral provider * fix: add missing translations for Mistral embedding provider * fix: address PR review feedback - improve translations and add clarifying comment --- packages/types/src/codebase-index.ts | 4 +- packages/types/src/global-settings.ts | 1 + src/core/webview/webviewMessageHandler.ts | 8 + src/i18n/locales/ca/embeddings.json | 1 + src/i18n/locales/de/embeddings.json | 1 + src/i18n/locales/en/embeddings.json | 1 + src/i18n/locales/es/embeddings.json | 1 + src/i18n/locales/fr/embeddings.json | 1 + src/i18n/locales/hi/embeddings.json | 1 + src/i18n/locales/id/embeddings.json | 1 + src/i18n/locales/it/embeddings.json | 1 + src/i18n/locales/ja/embeddings.json | 1 + src/i18n/locales/ko/embeddings.json | 1 + src/i18n/locales/nl/embeddings.json | 1 + src/i18n/locales/pl/embeddings.json | 1 + src/i18n/locales/pt-BR/embeddings.json | 1 + src/i18n/locales/ru/embeddings.json | 1 + src/i18n/locales/tr/embeddings.json | 1 + src/i18n/locales/vi/embeddings.json | 1 + src/i18n/locales/zh-CN/embeddings.json | 1 + src/i18n/locales/zh-TW/embeddings.json | 1 + src/services/code-index/config-manager.ts | 24 +++ .../embedders/__tests__/mistral.spec.ts | 193 ++++++++++++++++++ src/services/code-index/embedders/mistral.ts | 91 +++++++++ src/services/code-index/interfaces/config.ts | 2 + .../code-index/interfaces/embedder.ts | 2 +- src/services/code-index/interfaces/manager.ts | 2 +- src/services/code-index/service-factory.ts | 6 + src/shared/WebviewMessage.ts | 3 +- src/shared/embeddingModels.ts | 8 +- .../src/components/chat/CodeIndexPopover.tsx | 85 +++++++- webview-ui/src/i18n/locales/ca/settings.json | 4 + webview-ui/src/i18n/locales/de/settings.json | 4 + webview-ui/src/i18n/locales/en/settings.json | 4 + webview-ui/src/i18n/locales/es/settings.json | 4 + webview-ui/src/i18n/locales/fr/settings.json | 4 + webview-ui/src/i18n/locales/hi/settings.json | 4 + webview-ui/src/i18n/locales/id/settings.json | 4 + webview-ui/src/i18n/locales/it/settings.json | 4 + webview-ui/src/i18n/locales/ja/settings.json | 4 + webview-ui/src/i18n/locales/ko/settings.json | 4 + webview-ui/src/i18n/locales/nl/settings.json | 4 + webview-ui/src/i18n/locales/pl/settings.json | 4 + .../src/i18n/locales/pt-BR/settings.json | 4 + webview-ui/src/i18n/locales/ru/settings.json | 4 + webview-ui/src/i18n/locales/tr/settings.json | 4 + webview-ui/src/i18n/locales/vi/settings.json | 4 + .../src/i18n/locales/zh-CN/settings.json | 4 + .../src/i18n/locales/zh-TW/settings.json | 4 + 49 files changed, 513 insertions(+), 6 deletions(-) create mode 100644 src/services/code-index/embedders/__tests__/mistral.spec.ts create mode 100644 src/services/code-index/embedders/mistral.ts diff --git a/packages/types/src/codebase-index.ts b/packages/types/src/codebase-index.ts index 0ad19d8676..89d5b168d7 100644 --- a/packages/types/src/codebase-index.ts +++ b/packages/types/src/codebase-index.ts @@ -21,7 +21,7 @@ export const CODEBASE_INDEX_DEFAULTS = { export const codebaseIndexConfigSchema = z.object({ codebaseIndexEnabled: z.boolean().optional(), codebaseIndexQdrantUrl: z.string().optional(), - codebaseIndexEmbedderProvider: z.enum(["openai", "ollama", "openai-compatible", "gemini"]).optional(), + codebaseIndexEmbedderProvider: z.enum(["openai", "ollama", "openai-compatible", "gemini", "mistral"]).optional(), codebaseIndexEmbedderBaseUrl: z.string().optional(), codebaseIndexEmbedderModelId: z.string().optional(), codebaseIndexEmbedderModelDimension: z.number().optional(), @@ -47,6 +47,7 @@ export const codebaseIndexModelsSchema = z.object({ ollama: z.record(z.string(), z.object({ dimension: z.number() })).optional(), "openai-compatible": z.record(z.string(), z.object({ dimension: z.number() })).optional(), gemini: z.record(z.string(), z.object({ dimension: z.number() })).optional(), + mistral: z.record(z.string(), z.object({ dimension: z.number() })).optional(), }) export type CodebaseIndexModels = z.infer @@ -62,6 +63,7 @@ export const codebaseIndexProviderSchema = z.object({ codebaseIndexOpenAiCompatibleApiKey: z.string().optional(), codebaseIndexOpenAiCompatibleModelDimension: z.number().optional(), codebaseIndexGeminiApiKey: z.string().optional(), + codebaseIndexMistralApiKey: z.string().optional(), }) export type CodebaseIndexProvider = z.infer diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index bddb94cc9e..106d1f6e33 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -162,6 +162,7 @@ export const SECRET_STATE_KEYS = [ "codeIndexQdrantApiKey", "codebaseIndexOpenAiCompatibleApiKey", "codebaseIndexGeminiApiKey", + "codebaseIndexMistralApiKey", ] as const satisfies readonly (keyof ProviderSettings)[] export type SecretState = Pick diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 1bb76734db..fe4ede0c22 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1970,6 +1970,12 @@ export const webviewMessageHandler = async ( settings.codebaseIndexGeminiApiKey, ) } + if (settings.codebaseIndexMistralApiKey !== undefined) { + await provider.contextProxy.storeSecret( + "codebaseIndexMistralApiKey", + settings.codebaseIndexMistralApiKey, + ) + } // Send success response first - settings are saved regardless of validation await provider.postMessageToWebview({ @@ -2062,6 +2068,7 @@ export const webviewMessageHandler = async ( "codebaseIndexOpenAiCompatibleApiKey", )) const hasGeminiApiKey = !!(await provider.context.secrets.get("codebaseIndexGeminiApiKey")) + const hasMistralApiKey = !!(await provider.context.secrets.get("codebaseIndexMistralApiKey")) provider.postMessageToWebview({ type: "codeIndexSecretStatus", @@ -2070,6 +2077,7 @@ export const webviewMessageHandler = async ( hasQdrantApiKey, hasOpenAiCompatibleApiKey, hasGeminiApiKey, + hasMistralApiKey, }, }) break diff --git a/src/i18n/locales/ca/embeddings.json b/src/i18n/locales/ca/embeddings.json index 5deed252bf..651bc2b80f 100644 --- a/src/i18n/locales/ca/embeddings.json +++ b/src/i18n/locales/ca/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "Falta la configuració d'Ollama per crear l'embedder", "openAiCompatibleConfigMissing": "Falta la configuració compatible amb OpenAI per crear l'embedder", "geminiConfigMissing": "Falta la configuració de Gemini per crear l'embedder", + "mistralConfigMissing": "Falta la configuració de Mistral per crear l'embedder", "invalidEmbedderType": "Tipus d'embedder configurat no vàlid: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "No s'ha pogut determinar la dimensió del vector per al model '{{modelId}}' amb el proveïdor '{{provider}}'. Assegura't que la 'Dimensió d'incrustació' estigui configurada correctament als paràmetres del proveïdor compatible amb OpenAI.", "vectorDimensionNotDetermined": "No s'ha pogut determinar la dimensió del vector per al model '{{modelId}}' amb el proveïdor '{{provider}}'. Comprova els perfils del model o la configuració.", diff --git a/src/i18n/locales/de/embeddings.json b/src/i18n/locales/de/embeddings.json index 74381747e1..167abc516c 100644 --- a/src/i18n/locales/de/embeddings.json +++ b/src/i18n/locales/de/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "Ollama-Konfiguration fehlt für die Erstellung des Embedders", "openAiCompatibleConfigMissing": "OpenAI-kompatible Konfiguration fehlt für die Erstellung des Embedders", "geminiConfigMissing": "Gemini-Konfiguration fehlt für die Erstellung des Embedders", + "mistralConfigMissing": "Mistral-Konfiguration fehlt für die Erstellung des Embedders", "invalidEmbedderType": "Ungültiger Embedder-Typ konfiguriert: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "Konnte die Vektordimension für Modell '{{modelId}}' mit Anbieter '{{provider}}' nicht bestimmen. Stelle sicher, dass die 'Embedding-Dimension' in den OpenAI-kompatiblen Anbietereinstellungen korrekt eingestellt ist.", "vectorDimensionNotDetermined": "Konnte die Vektordimension für Modell '{{modelId}}' mit Anbieter '{{provider}}' nicht bestimmen. Überprüfe die Modellprofile oder Konfiguration.", diff --git a/src/i18n/locales/en/embeddings.json b/src/i18n/locales/en/embeddings.json index 96b3b2dbea..270a8d193b 100644 --- a/src/i18n/locales/en/embeddings.json +++ b/src/i18n/locales/en/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "Ollama configuration missing for embedder creation", "openAiCompatibleConfigMissing": "OpenAI Compatible configuration missing for embedder creation", "geminiConfigMissing": "Gemini configuration missing for embedder creation", + "mistralConfigMissing": "Mistral configuration missing for embedder creation", "invalidEmbedderType": "Invalid embedder type configured: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "Could not determine vector dimension for model '{{modelId}}' with provider '{{provider}}'. Please ensure the 'Embedding Dimension' is correctly set in the OpenAI-Compatible provider settings.", "vectorDimensionNotDetermined": "Could not determine vector dimension for model '{{modelId}}' with provider '{{provider}}'. Check model profiles or configuration.", diff --git a/src/i18n/locales/es/embeddings.json b/src/i18n/locales/es/embeddings.json index e47db420eb..06478f1d50 100644 --- a/src/i18n/locales/es/embeddings.json +++ b/src/i18n/locales/es/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "Falta la configuración de Ollama para crear el incrustador", "openAiCompatibleConfigMissing": "Falta la configuración compatible con OpenAI para crear el incrustador", "geminiConfigMissing": "Falta la configuración de Gemini para crear el incrustador", + "mistralConfigMissing": "Falta la configuración de Mistral para la creación del incrustador", "invalidEmbedderType": "Tipo de incrustador configurado inválido: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "No se pudo determinar la dimensión del vector para el modelo '{{modelId}}' con el proveedor '{{provider}}'. Asegúrate de que la 'Dimensión de incrustación' esté configurada correctamente en los ajustes del proveedor compatible con OpenAI.", "vectorDimensionNotDetermined": "No se pudo determinar la dimensión del vector para el modelo '{{modelId}}' con el proveedor '{{provider}}'. Verifica los perfiles del modelo o la configuración.", diff --git a/src/i18n/locales/fr/embeddings.json b/src/i18n/locales/fr/embeddings.json index c63d3a7fbc..167d093e7a 100644 --- a/src/i18n/locales/fr/embeddings.json +++ b/src/i18n/locales/fr/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "Configuration Ollama manquante pour la création de l'embedder", "openAiCompatibleConfigMissing": "Configuration compatible OpenAI manquante pour la création de l'embedder", "geminiConfigMissing": "Configuration Gemini manquante pour la création de l'embedder", + "mistralConfigMissing": "Configuration Mistral manquante pour la création de l'embedder", "invalidEmbedderType": "Type d'embedder configuré invalide : {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "Impossible de déterminer la dimension du vecteur pour le modèle '{{modelId}}' avec le fournisseur '{{provider}}'. Assure-toi que la 'Dimension d'embedding' est correctement définie dans les paramètres du fournisseur compatible OpenAI.", "vectorDimensionNotDetermined": "Impossible de déterminer la dimension du vecteur pour le modèle '{{modelId}}' avec le fournisseur '{{provider}}'. Vérifie les profils du modèle ou la configuration.", diff --git a/src/i18n/locales/hi/embeddings.json b/src/i18n/locales/hi/embeddings.json index 15709fd700..ad24cfe9d1 100644 --- a/src/i18n/locales/hi/embeddings.json +++ b/src/i18n/locales/hi/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "एम्बेडर बनाने के लिए Ollama कॉन्फ़िगरेशन गायब है", "openAiCompatibleConfigMissing": "एम्बेडर बनाने के लिए OpenAI संगत कॉन्फ़िगरेशन गायब है", "geminiConfigMissing": "एम्बेडर बनाने के लिए Gemini कॉन्फ़िगरेशन गायब है", + "mistralConfigMissing": "एम्बेडर निर्माण के लिए मिस्ट्रल कॉन्फ़िगरेशन गायब है", "invalidEmbedderType": "अमान्य एम्बेडर प्रकार कॉन्फ़िगर किया गया: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "प्रदाता '{{provider}}' के साथ मॉडल '{{modelId}}' के लिए वेक्टर आयाम निर्धारित नहीं कर सका। कृपया सुनिश्चित करें कि OpenAI-संगत प्रदाता सेटिंग्स में 'एम्बेडिंग आयाम' सही तरीके से सेट है।", "vectorDimensionNotDetermined": "प्रदाता '{{provider}}' के साथ मॉडल '{{modelId}}' के लिए वेक्टर आयाम निर्धारित नहीं कर सका। मॉडल प्रोफ़ाइल या कॉन्फ़िगरेशन की जांच करें।", diff --git a/src/i18n/locales/id/embeddings.json b/src/i18n/locales/id/embeddings.json index e78d39d1ab..997c6e8018 100644 --- a/src/i18n/locales/id/embeddings.json +++ b/src/i18n/locales/id/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "Konfigurasi Ollama tidak ada untuk membuat embedder", "openAiCompatibleConfigMissing": "Konfigurasi yang kompatibel dengan OpenAI tidak ada untuk membuat embedder", "geminiConfigMissing": "Konfigurasi Gemini tidak ada untuk membuat embedder", + "mistralConfigMissing": "Konfigurasi Mistral hilang untuk pembuatan embedder", "invalidEmbedderType": "Tipe embedder yang dikonfigurasi tidak valid: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "Tidak dapat menentukan dimensi vektor untuk model '{{modelId}}' dengan penyedia '{{provider}}'. Pastikan 'Dimensi Embedding' diatur dengan benar di pengaturan penyedia yang kompatibel dengan OpenAI.", "vectorDimensionNotDetermined": "Tidak dapat menentukan dimensi vektor untuk model '{{modelId}}' dengan penyedia '{{provider}}'. Periksa profil model atau konfigurasi.", diff --git a/src/i18n/locales/it/embeddings.json b/src/i18n/locales/it/embeddings.json index 679b17a25e..1bc406aecb 100644 --- a/src/i18n/locales/it/embeddings.json +++ b/src/i18n/locales/it/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "Configurazione Ollama mancante per la creazione dell'embedder", "openAiCompatibleConfigMissing": "Configurazione compatibile con OpenAI mancante per la creazione dell'embedder", "geminiConfigMissing": "Configurazione Gemini mancante per la creazione dell'embedder", + "mistralConfigMissing": "Configurazione di Mistral mancante per la creazione dell'embedder", "invalidEmbedderType": "Tipo di embedder configurato non valido: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "Impossibile determinare la dimensione del vettore per il modello '{{modelId}}' con il provider '{{provider}}'. Assicurati che la 'Dimensione di embedding' sia impostata correttamente nelle impostazioni del provider compatibile con OpenAI.", "vectorDimensionNotDetermined": "Impossibile determinare la dimensione del vettore per il modello '{{modelId}}' con il provider '{{provider}}'. Controlla i profili del modello o la configurazione.", diff --git a/src/i18n/locales/ja/embeddings.json b/src/i18n/locales/ja/embeddings.json index 89136eb1cc..7152eb52df 100644 --- a/src/i18n/locales/ja/embeddings.json +++ b/src/i18n/locales/ja/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "エンベッダー作成のためのOllama設定がありません", "openAiCompatibleConfigMissing": "エンベッダー作成のためのOpenAI互換設定がありません", "geminiConfigMissing": "エンベッダー作成のためのGemini設定がありません", + "mistralConfigMissing": "エンベッダー作成のためのMistral設定がありません", "invalidEmbedderType": "無効なエンベッダータイプが設定されています: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "プロバイダー '{{provider}}' のモデル '{{modelId}}' の埋め込み次元を決定できませんでした。OpenAI互換プロバイダー設定で「埋め込み次元」が正しく設定されていることを確認してください。", "vectorDimensionNotDetermined": "プロバイダー '{{provider}}' のモデル '{{modelId}}' の埋め込み次元を決定できませんでした。モデルプロファイルまたは設定を確認してください。", diff --git a/src/i18n/locales/ko/embeddings.json b/src/i18n/locales/ko/embeddings.json index 7129883ad7..f1c40f66bc 100644 --- a/src/i18n/locales/ko/embeddings.json +++ b/src/i18n/locales/ko/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "임베더 생성을 위한 Ollama 구성이 누락되었습니다", "openAiCompatibleConfigMissing": "임베더 생성을 위한 OpenAI 호환 구성이 누락되었습니다", "geminiConfigMissing": "임베더 생성을 위한 Gemini 구성이 누락되었습니다", + "mistralConfigMissing": "임베더 생성을 위한 Mistral 구성이 없습니다", "invalidEmbedderType": "잘못된 임베더 유형이 구성되었습니다: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "프로바이더 '{{provider}}'의 모델 '{{modelId}}'에 대한 벡터 차원을 결정할 수 없습니다. OpenAI 호환 프로바이더 설정에서 '임베딩 차원'이 올바르게 설정되어 있는지 확인하세요.", "vectorDimensionNotDetermined": "프로바이더 '{{provider}}'의 모델 '{{modelId}}'에 대한 벡터 차원을 결정할 수 없습니다. 모델 프로필 또는 구성을 확인하세요.", diff --git a/src/i18n/locales/nl/embeddings.json b/src/i18n/locales/nl/embeddings.json index ede20774ac..19b7bfeaa2 100644 --- a/src/i18n/locales/nl/embeddings.json +++ b/src/i18n/locales/nl/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "Ollama-configuratie ontbreekt voor het maken van embedder", "openAiCompatibleConfigMissing": "OpenAI-compatibele configuratie ontbreekt voor het maken van embedder", "geminiConfigMissing": "Gemini-configuratie ontbreekt voor het maken van embedder", + "mistralConfigMissing": "Mistral-configuratie ontbreekt voor het maken van de embedder", "invalidEmbedderType": "Ongeldig embedder-type geconfigureerd: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "Kan de vectordimensie voor model '{{modelId}}' met provider '{{provider}}' niet bepalen. Zorg ervoor dat de 'Embedding Dimensie' correct is ingesteld in de OpenAI-compatibele provider-instellingen.", "vectorDimensionNotDetermined": "Kan de vectordimensie voor model '{{modelId}}' met provider '{{provider}}' niet bepalen. Controleer modelprofielen of configuratie.", diff --git a/src/i18n/locales/pl/embeddings.json b/src/i18n/locales/pl/embeddings.json index 70279021bd..46e761cb8b 100644 --- a/src/i18n/locales/pl/embeddings.json +++ b/src/i18n/locales/pl/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "Brak konfiguracji Ollama do utworzenia embeddera", "openAiCompatibleConfigMissing": "Brak konfiguracji kompatybilnej z OpenAI do utworzenia embeddera", "geminiConfigMissing": "Brak konfiguracji Gemini do utworzenia embeddera", + "mistralConfigMissing": "Brak konfiguracji Mistral do utworzenia embeddera", "invalidEmbedderType": "Skonfigurowano nieprawidłowy typ embeddera: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "Nie można określić wymiaru wektora dla modelu '{{modelId}}' z dostawcą '{{provider}}'. Upewnij się, że 'Wymiar osadzania' jest poprawnie ustawiony w ustawieniach dostawcy kompatybilnego z OpenAI.", "vectorDimensionNotDetermined": "Nie można określić wymiaru wektora dla modelu '{{modelId}}' z dostawcą '{{provider}}'. Sprawdź profile modelu lub konfigurację.", diff --git a/src/i18n/locales/pt-BR/embeddings.json b/src/i18n/locales/pt-BR/embeddings.json index aea1bb5007..816b1ecded 100644 --- a/src/i18n/locales/pt-BR/embeddings.json +++ b/src/i18n/locales/pt-BR/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "Configuração do Ollama ausente para criação do embedder", "openAiCompatibleConfigMissing": "Configuração compatível com OpenAI ausente para criação do embedder", "geminiConfigMissing": "Configuração do Gemini ausente para criação do embedder", + "mistralConfigMissing": "Configuração do Mistral ausente para a criação do embedder", "invalidEmbedderType": "Tipo de embedder configurado inválido: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "Não foi possível determinar a dimensão do vetor para o modelo '{{modelId}}' com o provedor '{{provider}}'. Certifique-se de que a 'Dimensão de Embedding' esteja configurada corretamente nas configurações do provedor compatível com OpenAI.", "vectorDimensionNotDetermined": "Não foi possível determinar a dimensão do vetor para o modelo '{{modelId}}' com o provedor '{{provider}}'. Verifique os perfis do modelo ou a configuração.", diff --git a/src/i18n/locales/ru/embeddings.json b/src/i18n/locales/ru/embeddings.json index a724539b76..fb1688e2ca 100644 --- a/src/i18n/locales/ru/embeddings.json +++ b/src/i18n/locales/ru/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "Отсутствует конфигурация Ollama для создания эмбеддера", "openAiCompatibleConfigMissing": "Отсутствует конфигурация, совместимая с OpenAI, для создания эмбеддера", "geminiConfigMissing": "Отсутствует конфигурация Gemini для создания эмбеддера", + "mistralConfigMissing": "Конфигурация Mistral отсутствует для создания эмбеддера", "invalidEmbedderType": "Настроен недопустимый тип эмбеддера: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "Не удалось определить размерность вектора для модели '{{modelId}}' с провайдером '{{provider}}'. Убедитесь, что 'Размерность эмбеддинга' правильно установлена в настройках провайдера, совместимого с OpenAI.", "vectorDimensionNotDetermined": "Не удалось определить размерность вектора для модели '{{modelId}}' с провайдером '{{provider}}'. Проверьте профили модели или конфигурацию.", diff --git a/src/i18n/locales/tr/embeddings.json b/src/i18n/locales/tr/embeddings.json index 3e115ce103..5023190929 100644 --- a/src/i18n/locales/tr/embeddings.json +++ b/src/i18n/locales/tr/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "Gömücü oluşturmak için Ollama yapılandırması eksik", "openAiCompatibleConfigMissing": "Gömücü oluşturmak için OpenAI uyumlu yapılandırması eksik", "geminiConfigMissing": "Gömücü oluşturmak için Gemini yapılandırması eksik", + "mistralConfigMissing": "Gömücü oluşturmak için Mistral yapılandırması eksik", "invalidEmbedderType": "Geçersiz gömücü türü yapılandırıldı: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "'{{provider}}' sağlayıcısı ile '{{modelId}}' modeli için vektör boyutu belirlenemedi. OpenAI uyumlu sağlayıcı ayarlarında 'Gömme Boyutu'nun doğru ayarlandığından emin ol.", "vectorDimensionNotDetermined": "'{{provider}}' sağlayıcısı ile '{{modelId}}' modeli için vektör boyutu belirlenemedi. Model profillerini veya yapılandırmayı kontrol et.", diff --git a/src/i18n/locales/vi/embeddings.json b/src/i18n/locales/vi/embeddings.json index 9ef61105fa..626f0f6862 100644 --- a/src/i18n/locales/vi/embeddings.json +++ b/src/i18n/locales/vi/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "Thiếu cấu hình Ollama để tạo embedder", "openAiCompatibleConfigMissing": "Thiếu cấu hình tương thích OpenAI để tạo embedder", "geminiConfigMissing": "Thiếu cấu hình Gemini để tạo embedder", + "mistralConfigMissing": "Thiếu cấu hình Mistral để tạo trình nhúng", "invalidEmbedderType": "Loại embedder được cấu hình không hợp lệ: {{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "Không thể xác định kích thước vector cho mô hình '{{modelId}}' với nhà cung cấp '{{provider}}'. Hãy đảm bảo 'Kích thước Embedding' được cài đặt đúng trong cài đặt nhà cung cấp tương thích OpenAI.", "vectorDimensionNotDetermined": "Không thể xác định kích thước vector cho mô hình '{{modelId}}' với nhà cung cấp '{{provider}}'. Kiểm tra hồ sơ mô hình hoặc cấu hình.", diff --git a/src/i18n/locales/zh-CN/embeddings.json b/src/i18n/locales/zh-CN/embeddings.json index d3ded6e5a2..3247631bb2 100644 --- a/src/i18n/locales/zh-CN/embeddings.json +++ b/src/i18n/locales/zh-CN/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "创建嵌入器缺少 Ollama 配置", "openAiCompatibleConfigMissing": "创建嵌入器缺少 OpenAI 兼容配置", "geminiConfigMissing": "创建嵌入器缺少 Gemini 配置", + "mistralConfigMissing": "创建嵌入器时缺少 Mistral 配置", "invalidEmbedderType": "配置的嵌入器类型无效:{{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "无法确定提供商 '{{provider}}' 的模型 '{{modelId}}' 的向量维度。请确保在 OpenAI 兼容提供商设置中正确设置了「嵌入维度」。", "vectorDimensionNotDetermined": "无法确定提供商 '{{provider}}' 的模型 '{{modelId}}' 的向量维度。请检查模型配置文件或配置。", diff --git a/src/i18n/locales/zh-TW/embeddings.json b/src/i18n/locales/zh-TW/embeddings.json index 5ab5dcb292..b3b3231d4a 100644 --- a/src/i18n/locales/zh-TW/embeddings.json +++ b/src/i18n/locales/zh-TW/embeddings.json @@ -46,6 +46,7 @@ "ollamaConfigMissing": "建立嵌入器缺少 Ollama 設定", "openAiCompatibleConfigMissing": "建立嵌入器缺少 OpenAI 相容設定", "geminiConfigMissing": "建立嵌入器缺少 Gemini 設定", + "mistralConfigMissing": "建立嵌入器時缺少 Mistral 設定", "invalidEmbedderType": "設定的嵌入器類型無效:{{embedderProvider}}", "vectorDimensionNotDeterminedOpenAiCompatible": "無法確定提供商 '{{provider}}' 的模型 '{{modelId}}' 的向量維度。請確保在 OpenAI 相容提供商設定中正確設定了「嵌入維度」。", "vectorDimensionNotDetermined": "無法確定提供商 '{{provider}}' 的模型 '{{modelId}}' 的向量維度。請檢查模型設定檔或設定。", diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 9958f456c3..1723f1c2a0 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -18,6 +18,7 @@ export class CodeIndexConfigManager { private ollamaOptions?: ApiHandlerOptions private openAiCompatibleOptions?: { baseUrl: string; apiKey: string } private geminiOptions?: { apiKey: string } + private mistralOptions?: { apiKey: string } private qdrantUrl?: string = "http://localhost:6333" private qdrantApiKey?: string private searchMinScore?: number @@ -67,6 +68,7 @@ export class CodeIndexConfigManager { const openAiCompatibleBaseUrl = codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl ?? "" const openAiCompatibleApiKey = this.contextProxy?.getSecret("codebaseIndexOpenAiCompatibleApiKey") ?? "" const geminiApiKey = this.contextProxy?.getSecret("codebaseIndexGeminiApiKey") ?? "" + const mistralApiKey = this.contextProxy?.getSecret("codebaseIndexMistralApiKey") ?? "" // Update instance variables with configuration this.codebaseIndexEnabled = codebaseIndexEnabled ?? true @@ -100,6 +102,8 @@ export class CodeIndexConfigManager { this.embedderProvider = "openai-compatible" } else if (codebaseIndexEmbedderProvider === "gemini") { this.embedderProvider = "gemini" + } else if (codebaseIndexEmbedderProvider === "mistral") { + this.embedderProvider = "mistral" } else { this.embedderProvider = "openai" } @@ -119,6 +123,7 @@ export class CodeIndexConfigManager { : undefined this.geminiOptions = geminiApiKey ? { apiKey: geminiApiKey } : undefined + this.mistralOptions = mistralApiKey ? { apiKey: mistralApiKey } : undefined } /** @@ -135,6 +140,7 @@ export class CodeIndexConfigManager { ollamaOptions?: ApiHandlerOptions openAiCompatibleOptions?: { baseUrl: string; apiKey: string } geminiOptions?: { apiKey: string } + mistralOptions?: { apiKey: string } qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number @@ -153,6 +159,7 @@ export class CodeIndexConfigManager { openAiCompatibleBaseUrl: this.openAiCompatibleOptions?.baseUrl ?? "", openAiCompatibleApiKey: this.openAiCompatibleOptions?.apiKey ?? "", geminiApiKey: this.geminiOptions?.apiKey ?? "", + mistralApiKey: this.mistralOptions?.apiKey ?? "", qdrantUrl: this.qdrantUrl ?? "", qdrantApiKey: this.qdrantApiKey ?? "", } @@ -176,6 +183,7 @@ export class CodeIndexConfigManager { ollamaOptions: this.ollamaOptions, openAiCompatibleOptions: this.openAiCompatibleOptions, geminiOptions: this.geminiOptions, + mistralOptions: this.mistralOptions, qdrantUrl: this.qdrantUrl, qdrantApiKey: this.qdrantApiKey, searchMinScore: this.currentSearchMinScore, @@ -208,6 +216,11 @@ export class CodeIndexConfigManager { const qdrantUrl = this.qdrantUrl const isConfigured = !!(apiKey && qdrantUrl) return isConfigured + } else if (this.embedderProvider === "mistral") { + const apiKey = this.mistralOptions?.apiKey + const qdrantUrl = this.qdrantUrl + const isConfigured = !!(apiKey && qdrantUrl) + return isConfigured } return false // Should not happen if embedderProvider is always set correctly } @@ -241,6 +254,7 @@ export class CodeIndexConfigManager { const prevOpenAiCompatibleApiKey = prev?.openAiCompatibleApiKey ?? "" const prevModelDimension = prev?.modelDimension const prevGeminiApiKey = prev?.geminiApiKey ?? "" + const prevMistralApiKey = prev?.mistralApiKey ?? "" const prevQdrantUrl = prev?.qdrantUrl ?? "" const prevQdrantApiKey = prev?.qdrantApiKey ?? "" @@ -277,6 +291,7 @@ export class CodeIndexConfigManager { const currentOpenAiCompatibleApiKey = this.openAiCompatibleOptions?.apiKey ?? "" const currentModelDimension = this.modelDimension const currentGeminiApiKey = this.geminiOptions?.apiKey ?? "" + const currentMistralApiKey = this.mistralOptions?.apiKey ?? "" const currentQdrantUrl = this.qdrantUrl ?? "" const currentQdrantApiKey = this.qdrantApiKey ?? "" @@ -295,6 +310,14 @@ export class CodeIndexConfigManager { return true } + if (prevGeminiApiKey !== currentGeminiApiKey) { + return true + } + + if (prevMistralApiKey !== currentMistralApiKey) { + return true + } + // Check for model dimension changes (generic for all providers) if (prevModelDimension !== currentModelDimension) { return true @@ -351,6 +374,7 @@ export class CodeIndexConfigManager { ollamaOptions: this.ollamaOptions, openAiCompatibleOptions: this.openAiCompatibleOptions, geminiOptions: this.geminiOptions, + mistralOptions: this.mistralOptions, qdrantUrl: this.qdrantUrl, qdrantApiKey: this.qdrantApiKey, searchMinScore: this.currentSearchMinScore, diff --git a/src/services/code-index/embedders/__tests__/mistral.spec.ts b/src/services/code-index/embedders/__tests__/mistral.spec.ts new file mode 100644 index 0000000000..5085882503 --- /dev/null +++ b/src/services/code-index/embedders/__tests__/mistral.spec.ts @@ -0,0 +1,193 @@ +import { vitest, describe, it, expect, beforeEach } from "vitest" +import type { MockedClass } from "vitest" +import { MistralEmbedder } from "../mistral" +import { OpenAICompatibleEmbedder } from "../openai-compatible" + +// Mock the OpenAICompatibleEmbedder +vitest.mock("../openai-compatible") + +// Mock TelemetryService +vitest.mock("@roo-code/telemetry", () => ({ + TelemetryService: { + instance: { + captureEvent: vitest.fn(), + }, + }, +})) + +const MockedOpenAICompatibleEmbedder = OpenAICompatibleEmbedder as MockedClass + +describe("MistralEmbedder", () => { + let embedder: MistralEmbedder + + beforeEach(() => { + vitest.clearAllMocks() + }) + + describe("constructor", () => { + it("should create an instance with default model when no model specified", () => { + // Arrange + const apiKey = "test-mistral-api-key" + + // Act + embedder = new MistralEmbedder(apiKey) + + // Assert + expect(MockedOpenAICompatibleEmbedder).toHaveBeenCalledWith( + "https://api.mistral.ai/v1", + apiKey, + "codestral-embed-2505", + 8191, + ) + }) + + it("should create an instance with specified model", () => { + // Arrange + const apiKey = "test-mistral-api-key" + const modelId = "custom-embed-model" + + // Act + embedder = new MistralEmbedder(apiKey, modelId) + + // Assert + expect(MockedOpenAICompatibleEmbedder).toHaveBeenCalledWith( + "https://api.mistral.ai/v1", + apiKey, + "custom-embed-model", + 8191, + ) + }) + + it("should throw error when API key is not provided", () => { + // Act & Assert + expect(() => new MistralEmbedder("")).toThrow("validation.apiKeyRequired") + expect(() => new MistralEmbedder(null as any)).toThrow("validation.apiKeyRequired") + expect(() => new MistralEmbedder(undefined as any)).toThrow("validation.apiKeyRequired") + }) + }) + + describe("embedderInfo", () => { + it("should return correct embedder info", () => { + // Arrange + embedder = new MistralEmbedder("test-api-key") + + // Act + const info = embedder.embedderInfo + + // Assert + expect(info).toEqual({ + name: "mistral", + }) + }) + + describe("createEmbeddings", () => { + let mockCreateEmbeddings: any + + beforeEach(() => { + mockCreateEmbeddings = vitest.fn() + MockedOpenAICompatibleEmbedder.prototype.createEmbeddings = mockCreateEmbeddings + }) + + it("should use instance model when no model parameter provided", async () => { + // Arrange + embedder = new MistralEmbedder("test-api-key") + const texts = ["test text 1", "test text 2"] + const mockResponse = { + embeddings: [ + [0.1, 0.2], + [0.3, 0.4], + ], + } + mockCreateEmbeddings.mockResolvedValue(mockResponse) + + // Act + const result = await embedder.createEmbeddings(texts) + + // Assert + expect(mockCreateEmbeddings).toHaveBeenCalledWith(texts, "codestral-embed-2505") + expect(result).toEqual(mockResponse) + }) + + it("should use provided model parameter when specified", async () => { + // Arrange + embedder = new MistralEmbedder("test-api-key", "custom-embed-model") + const texts = ["test text 1", "test text 2"] + const mockResponse = { + embeddings: [ + [0.1, 0.2], + [0.3, 0.4], + ], + } + mockCreateEmbeddings.mockResolvedValue(mockResponse) + + // Act + const result = await embedder.createEmbeddings(texts, "codestral-embed-2505") + + // Assert + expect(mockCreateEmbeddings).toHaveBeenCalledWith(texts, "codestral-embed-2505") + expect(result).toEqual(mockResponse) + }) + + it("should handle errors from OpenAICompatibleEmbedder", async () => { + // Arrange + embedder = new MistralEmbedder("test-api-key") + const texts = ["test text"] + const error = new Error("Embedding failed") + mockCreateEmbeddings.mockRejectedValue(error) + + // Act & Assert + await expect(embedder.createEmbeddings(texts)).rejects.toThrow("Embedding failed") + }) + }) + }) + + describe("validateConfiguration", () => { + let mockValidateConfiguration: any + + beforeEach(() => { + mockValidateConfiguration = vitest.fn() + MockedOpenAICompatibleEmbedder.prototype.validateConfiguration = mockValidateConfiguration + }) + + it("should delegate validation to OpenAICompatibleEmbedder", async () => { + // Arrange + embedder = new MistralEmbedder("test-api-key") + mockValidateConfiguration.mockResolvedValue({ valid: true }) + + // Act + const result = await embedder.validateConfiguration() + + // Assert + expect(mockValidateConfiguration).toHaveBeenCalled() + expect(result).toEqual({ valid: true }) + }) + + it("should pass through validation errors from OpenAICompatibleEmbedder", async () => { + // Arrange + embedder = new MistralEmbedder("test-api-key") + mockValidateConfiguration.mockResolvedValue({ + valid: false, + error: "embeddings:validation.authenticationFailed", + }) + + // Act + const result = await embedder.validateConfiguration() + + // Assert + expect(mockValidateConfiguration).toHaveBeenCalled() + expect(result).toEqual({ + valid: false, + error: "embeddings:validation.authenticationFailed", + }) + }) + + it("should handle validation exceptions", async () => { + // Arrange + embedder = new MistralEmbedder("test-api-key") + mockValidateConfiguration.mockRejectedValue(new Error("Validation failed")) + + // Act & Assert + await expect(embedder.validateConfiguration()).rejects.toThrow("Validation failed") + }) + }) +}) diff --git a/src/services/code-index/embedders/mistral.ts b/src/services/code-index/embedders/mistral.ts new file mode 100644 index 0000000000..c23bcbba1d --- /dev/null +++ b/src/services/code-index/embedders/mistral.ts @@ -0,0 +1,91 @@ +import { OpenAICompatibleEmbedder } from "./openai-compatible" +import { IEmbedder, EmbeddingResponse, EmbedderInfo } from "../interfaces/embedder" +import { MAX_ITEM_TOKENS } from "../constants" +import { t } from "../../../i18n" +import { TelemetryEventName } from "@roo-code/types" +import { TelemetryService } from "@roo-code/telemetry" + +/** + * Mistral embedder implementation that wraps the OpenAI Compatible embedder + * with configuration for Mistral's embedding API. + * + * Supported models: + * - codestral-embed-2505 (dimension: 1536) + */ +export class MistralEmbedder implements IEmbedder { + private readonly openAICompatibleEmbedder: OpenAICompatibleEmbedder + private static readonly MISTRAL_BASE_URL = "https://api.mistral.ai/v1" + private static readonly DEFAULT_MODEL = "codestral-embed-2505" + private readonly modelId: string + + /** + * Creates a new Mistral embedder + * @param apiKey The Mistral API key for authentication + * @param modelId The model ID to use (defaults to codestral-embed-2505) + */ + constructor(apiKey: string, modelId?: string) { + if (!apiKey) { + throw new Error(t("embeddings:validation.apiKeyRequired")) + } + + // Use provided model or default + this.modelId = modelId || MistralEmbedder.DEFAULT_MODEL + + // Create an OpenAI Compatible embedder with Mistral's configuration + this.openAICompatibleEmbedder = new OpenAICompatibleEmbedder( + MistralEmbedder.MISTRAL_BASE_URL, + apiKey, + this.modelId, + MAX_ITEM_TOKENS, // This is the max token limit (8191), not the embedding dimension + ) + } + + /** + * Creates embeddings for the given texts using Mistral's embedding API + * @param texts Array of text strings to embed + * @param model Optional model identifier (uses constructor model if not provided) + * @returns Promise resolving to embedding response + */ + async createEmbeddings(texts: string[], model?: string): Promise { + try { + // Use the provided model or fall back to the instance's model + const modelToUse = model || this.modelId + return await this.openAICompatibleEmbedder.createEmbeddings(texts, modelToUse) + } catch (error) { + TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + location: "MistralEmbedder:createEmbeddings", + }) + throw error + } + } + + /** + * Validates the Mistral embedder configuration by delegating to the underlying OpenAI-compatible embedder + * @returns Promise resolving to validation result with success status and optional error message + */ + async validateConfiguration(): Promise<{ valid: boolean; error?: string }> { + try { + // Delegate validation to the OpenAI-compatible embedder + // The error messages will be specific to Mistral since we're using Mistral's base URL + return await this.openAICompatibleEmbedder.validateConfiguration() + } catch (error) { + TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + location: "MistralEmbedder:validateConfiguration", + }) + throw error + } + } + + /** + * Returns information about this embedder + */ + get embedderInfo(): EmbedderInfo { + return { + name: "mistral", + } + } +} diff --git a/src/services/code-index/interfaces/config.ts b/src/services/code-index/interfaces/config.ts index 190a23e2a3..9098a60091 100644 --- a/src/services/code-index/interfaces/config.ts +++ b/src/services/code-index/interfaces/config.ts @@ -13,6 +13,7 @@ export interface CodeIndexConfig { ollamaOptions?: ApiHandlerOptions openAiCompatibleOptions?: { baseUrl: string; apiKey: string } geminiOptions?: { apiKey: string } + mistralOptions?: { apiKey: string } qdrantUrl?: string qdrantApiKey?: string searchMinScore?: number @@ -33,6 +34,7 @@ export type PreviousConfigSnapshot = { openAiCompatibleBaseUrl?: string openAiCompatibleApiKey?: string geminiApiKey?: string + mistralApiKey?: string qdrantUrl?: string qdrantApiKey?: string } diff --git a/src/services/code-index/interfaces/embedder.ts b/src/services/code-index/interfaces/embedder.ts index 0a74446d5e..c5653ea2b7 100644 --- a/src/services/code-index/interfaces/embedder.ts +++ b/src/services/code-index/interfaces/embedder.ts @@ -28,7 +28,7 @@ export interface EmbeddingResponse { } } -export type AvailableEmbedders = "openai" | "ollama" | "openai-compatible" | "gemini" +export type AvailableEmbedders = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" export interface EmbedderInfo { name: AvailableEmbedders diff --git a/src/services/code-index/interfaces/manager.ts b/src/services/code-index/interfaces/manager.ts index 70e3fd9765..fd3b2bfdda 100644 --- a/src/services/code-index/interfaces/manager.ts +++ b/src/services/code-index/interfaces/manager.ts @@ -70,7 +70,7 @@ export interface ICodeIndexManager { } export type IndexingState = "Standby" | "Indexing" | "Indexed" | "Error" -export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" +export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" export interface IndexProgressUpdate { systemStatus: IndexingState diff --git a/src/services/code-index/service-factory.ts b/src/services/code-index/service-factory.ts index b7951db7ac..68b0f5c0bc 100644 --- a/src/services/code-index/service-factory.ts +++ b/src/services/code-index/service-factory.ts @@ -3,6 +3,7 @@ import { OpenAiEmbedder } from "./embedders/openai" import { CodeIndexOllamaEmbedder } from "./embedders/ollama" import { OpenAICompatibleEmbedder } from "./embedders/openai-compatible" import { GeminiEmbedder } from "./embedders/gemini" +import { MistralEmbedder } from "./embedders/mistral" import { EmbedderProvider, getDefaultModelId, getModelDimension } from "../../shared/embeddingModels" import { QdrantVectorStore } from "./vector-store/qdrant-client" import { codeParser, DirectoryScanner, FileWatcher } from "./processors" @@ -64,6 +65,11 @@ export class CodeIndexServiceFactory { throw new Error(t("embeddings:serviceFactory.geminiConfigMissing")) } return new GeminiEmbedder(config.geminiOptions.apiKey, config.modelId) + } else if (provider === "mistral") { + if (!config.mistralOptions?.apiKey) { + throw new Error(t("embeddings:serviceFactory.mistralConfigMissing")) + } + return new MistralEmbedder(config.mistralOptions.apiKey, config.modelId) } throw new Error( diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index e983ce720d..0317c46987 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -244,7 +244,7 @@ export interface WebviewMessage { // Global state settings codebaseIndexEnabled: boolean codebaseIndexQdrantUrl: string - codebaseIndexEmbedderProvider: "openai" | "ollama" | "openai-compatible" | "gemini" + codebaseIndexEmbedderProvider: "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" codebaseIndexEmbedderBaseUrl?: string codebaseIndexEmbedderModelId: string codebaseIndexEmbedderModelDimension?: number // Generic dimension for all providers @@ -257,6 +257,7 @@ export interface WebviewMessage { codeIndexQdrantApiKey?: string codebaseIndexOpenAiCompatibleApiKey?: string codebaseIndexGeminiApiKey?: string + codebaseIndexMistralApiKey?: string } } diff --git a/src/shared/embeddingModels.ts b/src/shared/embeddingModels.ts index f387480c65..a3cd61e659 100644 --- a/src/shared/embeddingModels.ts +++ b/src/shared/embeddingModels.ts @@ -2,7 +2,7 @@ * Defines profiles for different embedding models, including their dimensions. */ -export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" // Add other providers as needed +export type EmbedderProvider = "openai" | "ollama" | "openai-compatible" | "gemini" | "mistral" // Add other providers as needed export interface EmbeddingModelProfile { dimension: number @@ -50,6 +50,9 @@ export const EMBEDDING_MODEL_PROFILES: EmbeddingModelProfiles = { "text-embedding-004": { dimension: 768 }, "gemini-embedding-001": { dimension: 3072, scoreThreshold: 0.4 }, }, + mistral: { + "codestral-embed-2505": { dimension: 1536, scoreThreshold: 0.4 }, + }, } /** @@ -137,6 +140,9 @@ export function getDefaultModelId(provider: EmbedderProvider): string { case "gemini": return "gemini-embedding-001" + case "mistral": + return "codestral-embed-2505" + default: // Fallback for unknown providers console.warn(`Unknown provider for default model ID: ${provider}. Falling back to OpenAI default.`) diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index 4385e2e844..d7683e8c7e 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -68,6 +68,7 @@ interface LocalCodeIndexSettings { codebaseIndexOpenAiCompatibleBaseUrl?: string codebaseIndexOpenAiCompatibleApiKey?: string codebaseIndexGeminiApiKey?: string + codebaseIndexMistralApiKey?: string } // Validation schema for codebase index settings @@ -126,6 +127,14 @@ const createValidationSchema = (provider: EmbedderProvider, t: any) => { .min(1, t("settings:codeIndex.validation.modelSelectionRequired")), }) + case "mistral": + return baseSchema.extend({ + codebaseIndexMistralApiKey: z.string().min(1, t("settings:codeIndex.validation.mistralApiKeyRequired")), + codebaseIndexEmbedderModelId: z + .string() + .min(1, t("settings:codeIndex.validation.modelSelectionRequired")), + }) + default: return baseSchema } @@ -169,6 +178,7 @@ export const CodeIndexPopover: React.FC = ({ codebaseIndexOpenAiCompatibleBaseUrl: "", codebaseIndexOpenAiCompatibleApiKey: "", codebaseIndexGeminiApiKey: "", + codebaseIndexMistralApiKey: "", }) // Initial settings state - stores the settings when popover opens @@ -202,6 +212,7 @@ export const CodeIndexPopover: React.FC = ({ codebaseIndexOpenAiCompatibleBaseUrl: codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl || "", codebaseIndexOpenAiCompatibleApiKey: "", codebaseIndexGeminiApiKey: "", + codebaseIndexMistralApiKey: "", } setInitialSettings(settings) setCurrentSettings(settings) @@ -293,6 +304,9 @@ export const CodeIndexPopover: React.FC = ({ if (!prev.codebaseIndexGeminiApiKey || prev.codebaseIndexGeminiApiKey === SECRET_PLACEHOLDER) { updated.codebaseIndexGeminiApiKey = secretStatus.hasGeminiApiKey ? SECRET_PLACEHOLDER : "" } + if (!prev.codebaseIndexMistralApiKey || prev.codebaseIndexMistralApiKey === SECRET_PLACEHOLDER) { + updated.codebaseIndexMistralApiKey = secretStatus.hasMistralApiKey ? SECRET_PLACEHOLDER : "" + } return updated } @@ -364,7 +378,8 @@ export const CodeIndexPopover: React.FC = ({ if ( key === "codeIndexOpenAiKey" || key === "codebaseIndexOpenAiCompatibleApiKey" || - key === "codebaseIndexGeminiApiKey" + key === "codebaseIndexGeminiApiKey" || + key === "codebaseIndexMistralApiKey" ) { dataToValidate[key] = "placeholder-valid" } @@ -606,6 +621,9 @@ export const CodeIndexPopover: React.FC = ({ {t("settings:codeIndex.geminiProvider")} + + {t("settings:codeIndex.mistralProvider")} + @@ -933,6 +951,71 @@ export const CodeIndexPopover: React.FC = ({ )} + {currentSettings.codebaseIndexEmbedderProvider === "mistral" && ( + <> +
+ + + updateSetting("codebaseIndexMistralApiKey", e.target.value) + } + placeholder={t("settings:codeIndex.mistralApiKeyPlaceholder")} + className={cn("w-full", { + "border-red-500": formErrors.codebaseIndexMistralApiKey, + })} + /> + {formErrors.codebaseIndexMistralApiKey && ( +

+ {formErrors.codebaseIndexMistralApiKey} +

+ )} +
+ +
+ + + updateSetting("codebaseIndexEmbedderModelId", e.target.value) + } + className={cn("w-full", { + "border-red-500": formErrors.codebaseIndexEmbedderModelId, + })}> + + {t("settings:codeIndex.selectModel")} + + {getAvailableModels().map((modelId) => { + const model = + codebaseIndexModels?.[ + currentSettings.codebaseIndexEmbedderProvider + ]?.[modelId] + return ( + + {modelId}{" "} + {model + ? t("settings:codeIndex.modelDimensions", { + dimension: model.dimension, + }) + : ""} + + ) + })} + + {formErrors.codebaseIndexEmbedderModelId && ( +

+ {formErrors.codebaseIndexEmbedderModelId} +

+ )} +
+ + )} + {/* Qdrant Settings */}
+
+ +
+ + setCachedStateField("terminalOutputCharacterLimit", value) + } + data-testid="terminal-output-character-limit-slider" + /> + {terminalOutputCharacterLimit ?? 50000} +
+
+ + + {" "} + + +
+
void terminalOutputLineLimit?: number setTerminalOutputLineLimit: (value: number) => void + terminalOutputCharacterLimit?: number + setTerminalOutputCharacterLimit: (value: number) => void mcpEnabled: boolean setMcpEnabled: (value: boolean) => void enableMcpServerCreation: boolean @@ -176,6 +178,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode browserViewportSize: "900x600", screenshotQuality: 75, terminalOutputLineLimit: 500, + terminalOutputCharacterLimit: 50000, terminalShellIntegrationTimeout: 4000, mcpEnabled: true, enableMcpServerCreation: false, @@ -410,6 +413,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setScreenshotQuality: (value) => setState((prevState) => ({ ...prevState, screenshotQuality: value })), setTerminalOutputLineLimit: (value) => setState((prevState) => ({ ...prevState, terminalOutputLineLimit: value })), + setTerminalOutputCharacterLimit: (value) => + setState((prevState) => ({ ...prevState, terminalOutputCharacterLimit: value })), setTerminalShellIntegrationTimeout: (value) => setState((prevState) => ({ ...prevState, terminalShellIntegrationTimeout: value })), setTerminalShellIntegrationDisabled: (value) => diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index df544bfa29..eaa83b1b0d 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -508,6 +508,10 @@ "label": "Límit de sortida de terminal", "description": "Nombre màxim de línies a incloure a la sortida del terminal en executar comandes. Quan s'excedeix, s'eliminaran línies del mig, estalviant token. <0>Més informació" }, + "outputCharacterLimit": { + "label": "Límit de caràcters del terminal", + "description": "Nombre màxim de caràcters a incloure en la sortida del terminal en executar ordres. Aquest límit té precedència sobre el límit de línies per evitar problemes de memòria amb línies extremadament llargues. Quan se superi, la sortida es truncarà. <0>Més informació" + }, "shellIntegrationTimeout": { "label": "Temps d'espera d'integració de shell del terminal", "description": "Temps màxim d'espera per a la inicialització de la integració de shell abans d'executar comandes. Per a usuaris amb temps d'inici de shell llargs, aquest valor pot necessitar ser augmentat si veieu errors \"Shell Integration Unavailable\" al terminal. <0>Més informació" diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 1e754537da..7f7c2c22b7 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -508,6 +508,10 @@ "label": "Terminal-Ausgabelimit", "description": "Maximale Anzahl von Zeilen, die in der Terminal-Ausgabe bei der Ausführung von Befehlen enthalten sein sollen. Bei Überschreitung werden Zeilen aus der Mitte entfernt, wodurch Token gespart werden. <0>Mehr erfahren" }, + "outputCharacterLimit": { + "label": "Terminal-Zeichenlimit", + "description": "Maximale Anzahl von Zeichen, die in die Terminalausgabe bei der Ausführung von Befehlen aufgenommen werden sollen. Dieses Limit hat Vorrang vor dem Zeilenlimit, um Speicherprobleme durch extrem lange Zeilen zu vermeiden. Bei Überschreitung wird die Ausgabe abgeschnitten. <0>Mehr erfahren" + }, "shellIntegrationTimeout": { "label": "Terminal-Shell-Integrationszeit-Limit", "description": "Maximale Wartezeit für die Shell-Integration, bevor Befehle ausgeführt werden. Für Benutzer mit langen Shell-Startzeiten musst du diesen Wert möglicherweise erhöhen, wenn du Fehler vom Typ \"Shell Integration Unavailable\" im Terminal siehst. <0>Mehr erfahren" diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 60a1974de1..7e3c2e3fcc 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -508,6 +508,10 @@ "label": "Terminal output limit", "description": "Maximum number of lines to include in terminal output when executing commands. When exceeded lines will be removed from the middle, saving tokens. <0>Learn more" }, + "outputCharacterLimit": { + "label": "Terminal character limit", + "description": "Maximum number of characters to include in terminal output when executing commands. This limit takes precedence over the line limit to prevent memory issues from extremely long lines. When exceeded, output will be truncated. <0>Learn more" + }, "shellIntegrationTimeout": { "label": "Terminal shell integration timeout", "description": "Maximum time to wait for shell integration to initialize before executing commands. For users with long shell startup times, this value may need to be increased if you see \"Shell Integration Unavailable\" errors in the terminal. <0>Learn more" diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index 2075367560..f00c2c9b42 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -508,6 +508,10 @@ "label": "Límite de salida de terminal", "description": "Número máximo de líneas a incluir en la salida del terminal al ejecutar comandos. Cuando se excede, se eliminarán líneas del medio, ahorrando token. <0>Más información" }, + "outputCharacterLimit": { + "label": "Límite de caracteres del terminal", + "description": "Número máximo de caracteres a incluir en la salida del terminal al ejecutar comandos. Este límite tiene prioridad sobre el límite de líneas para evitar problemas de memoria con líneas extremadamente largas. Cuando se excede, la salida se truncará. <0>Aprende más" + }, "shellIntegrationTimeout": { "label": "Tiempo de espera de integración del shell del terminal", "description": "Tiempo máximo de espera para la inicialización de la integración del shell antes de ejecutar comandos. Para usuarios con tiempos de inicio de shell largos, este valor puede necesitar ser aumentado si ve errores \"Shell Integration Unavailable\" en el terminal. <0>Más información" diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 5b1e489b3c..02dfac3552 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -508,6 +508,10 @@ "label": "Limite de sortie du terminal", "description": "Nombre maximum de lignes à inclure dans la sortie du terminal lors de l'exécution de commandes. Lorsque ce nombre est dépassé, les lignes seront supprimées du milieu, économisant des token. <0>En savoir plus" }, + "outputCharacterLimit": { + "label": "Limite de caractères du terminal", + "description": "Nombre maximum de caractères à inclure dans la sortie du terminal lors de l'exécution de commandes. Cette limite prévaut sur la limite de lignes pour éviter les problèmes de mémoire avec des lignes extrêmement longues. Lorsque cette limite est dépassée, la sortie sera tronquée. <0>En savoir plus" + }, "shellIntegrationTimeout": { "label": "Délai d'intégration du shell du terminal", "description": "Temps maximum d'attente pour l'initialisation de l'intégration du shell avant d'exécuter des commandes. Pour les utilisateurs avec des temps de démarrage de shell longs, cette valeur peut nécessiter d'être augmentée si vous voyez des erreurs \"Shell Integration Unavailable\" dans le terminal. <0>En savoir plus" diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 3f0f7bde16..2b7fd03d56 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -508,6 +508,10 @@ "label": "टर्मिनल आउटपुट सीमा", "description": "कमांड निष्पादित करते समय टर्मिनल आउटपुट में शामिल करने के लिए पंक्तियों की अधिकतम संख्या। पार होने पर पंक्तियाँ मध्य से हटा दी जाएंगी, token बचाते हुए। <0>अधिक जानें" }, + "outputCharacterLimit": { + "label": "टर्मिनल वर्ण सीमा", + "description": "कमांड निष्पादित करते समय टर्मिनल आउटपुट में शामिल किए जाने वाले वर्णों की अधिकतम संख्या। यह सीमा अत्यधिक लंबी लाइनों से मेमोरी समस्याओं को रोकने के लिए लाइन सीमा पर पूर्वता लेती है। जब यह सीमा पार हो जाती है, तो आउटपुट छोटा कर दिया जाएगा। <0>और जानें" + }, "shellIntegrationTimeout": { "label": "टर्मिनल शेल एकीकरण टाइमआउट", "description": "कमांड निष्पादित करने से पहले शेल एकीकरण के आरंभ होने के लिए प्रतीक्षा का अधिकतम समय। लंबे शेल स्टार्टअप समय वाले उपयोगकर्ताओं के लिए, यदि आप टर्मिनल में \"Shell Integration Unavailable\" त्रुटियाँ देखते हैं तो इस मान को बढ़ाने की आवश्यकता हो सकती है। <0>अधिक जानें" diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index b2ed679774..c6ba2728a7 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -512,6 +512,10 @@ "label": "Batas output terminal", "description": "Jumlah maksimum baris yang disertakan dalam output terminal saat mengeksekusi perintah. Ketika terlampaui, baris akan dihapus dari tengah, menghemat token. <0>Pelajari lebih lanjut" }, + "outputCharacterLimit": { + "label": "Batas karakter terminal", + "description": "Jumlah maksimum karakter yang akan disertakan dalam output terminal saat menjalankan perintah. Batas ini lebih diutamakan daripada batas baris untuk mencegah masalah memori dari baris yang sangat panjang. Ketika terlampaui, output akan dipotong. <0>Pelajari lebih lanjut" + }, "shellIntegrationTimeout": { "label": "Timeout integrasi shell terminal", "description": "Waktu maksimum untuk menunggu integrasi shell menginisialisasi sebelum mengeksekusi perintah. Untuk pengguna dengan waktu startup shell yang lama, nilai ini mungkin perlu ditingkatkan jika kamu melihat error \"Shell Integration Unavailable\" di terminal. <0>Pelajari lebih lanjut" diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index bf6022360c..26c776d60d 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -508,6 +508,10 @@ "label": "Limite output terminale", "description": "Numero massimo di righe da includere nell'output del terminale durante l'esecuzione dei comandi. Quando superato, le righe verranno rimosse dal centro, risparmiando token. <0>Scopri di più" }, + "outputCharacterLimit": { + "label": "Limite di caratteri del terminale", + "description": "Numero massimo di caratteri da includere nell'output del terminale durante l'esecuzione dei comandi. Questo limite ha la precedenza sul limite di righe per prevenire problemi di memoria causati da righe estremamente lunghe. Se superato, l'output verrà troncato. <0>Scopri di più" + }, "shellIntegrationTimeout": { "label": "Timeout integrazione shell del terminale", "description": "Tempo massimo di attesa per l'inizializzazione dell'integrazione della shell prima di eseguire i comandi. Per gli utenti con tempi di avvio della shell lunghi, questo valore potrebbe dover essere aumentato se si vedono errori \"Shell Integration Unavailable\" nel terminale. <0>Scopri di più" diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 0e61dbec7f..9eb4328c12 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -508,6 +508,10 @@ "label": "ターミナル出力制限", "description": "コマンド実行時にターミナル出力に含める最大行数。超過すると中央から行が削除され、tokenを節約します。 <0>詳細情報" }, + "outputCharacterLimit": { + "label": "ターミナルの文字数制限", + "description": "コマンド実行時にターミナル出力に含める最大文字数。この制限は、非常に長い行によるメモリ問題を回避するため、行数制限よりも優先されます。超過した場合、出力は切り捨てられます。<0>詳細" + }, "shellIntegrationTimeout": { "label": "ターミナルシェル統合タイムアウト", "description": "コマンドを実行する前にシェル統合の初期化を待つ最大時間。シェルの起動時間が長いユーザーの場合、ターミナルで「Shell Integration Unavailable」エラーが表示される場合は、この値を増やす必要があるかもしれません。 <0>詳細情報" diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index ca09d63818..18b054bbb8 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -508,6 +508,10 @@ "label": "터미널 출력 제한", "description": "명령 실행 시 터미널 출력에 포함할 최대 라인 수. 초과 시 중간에서 라인이 제거되어 token이 절약됩니다. <0>더 알아보기" }, + "outputCharacterLimit": { + "label": "터미널 문자 제한", + "description": "명령을 실행할 때 터미널 출력에 포함할 최대 문자 수입니다. 이 제한은 매우 긴 줄로 인한 메모리 문제를 방지하기 위해 줄 제한보다 우선합니다. 초과하면 출력이 잘립니다. <0>더 알아보기" + }, "shellIntegrationTimeout": { "label": "터미널 쉘 통합 타임아웃", "description": "명령을 실행하기 전에 쉘 통합이 초기화될 때까지 기다리는 최대 시간. 쉘 시작 시간이 긴 사용자의 경우, 터미널에서 \"Shell Integration Unavailable\" 오류가 표시되면 이 값을 늘려야 할 수 있습니다. <0>더 알아보기" diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 74ce568968..73ecf87d34 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -508,6 +508,10 @@ "label": "Terminaluitvoerlimiet", "description": "Maximaal aantal regels dat wordt opgenomen in de terminaluitvoer bij het uitvoeren van commando's. Overtollige regels worden uit het midden verwijderd om tokens te besparen. <0>Meer informatie" }, + "outputCharacterLimit": { + "label": "Tekenlimiet terminal", + "description": "Maximaal aantal tekens dat moet worden opgenomen in de terminaluitvoer bij het uitvoeren van commando's. Deze limiet heeft voorrang op de regellimiet om geheugenproblemen door extreem lange regels te voorkomen. Bij overschrijding wordt de uitvoer afgekapt. <0>Meer informatie" + }, "shellIntegrationTimeout": { "label": "Terminal shell-integratie timeout", "description": "Maximale wachttijd voor het initialiseren van shell-integratie voordat commando's worden uitgevoerd. Voor gebruikers met lange shell-opstarttijden moet deze waarde mogelijk worden verhoogd als je 'Shell Integration Unavailable'-fouten ziet in de terminal. <0>Meer informatie" diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index c6bbdfcde1..b0fb8efac3 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -508,6 +508,10 @@ "label": "Limit wyjścia terminala", "description": "Maksymalna liczba linii do uwzględnienia w wyjściu terminala podczas wykonywania poleceń. Po przekroczeniu linie będą usuwane ze środka, oszczędzając token. <0>Dowiedz się więcej" }, + "outputCharacterLimit": { + "label": "Limit znaków w terminalu", + "description": "Maksymalna liczba znaków do uwzględnienia w danych wyjściowych terminala podczas wykonywania poleceń. Limit ten ma pierwszeństwo przed limitem linii, aby zapobiec problemom z pamięcią spowodowanym przez bardzo długie linie. Po przekroczeniu limitu dane wyjściowe zostaną obcięte. <0>Dowiedz się więcej" + }, "shellIntegrationTimeout": { "label": "Limit czasu integracji powłoki terminala", "description": "Maksymalny czas oczekiwania na inicjalizację integracji powłoki przed wykonaniem poleceń. Dla użytkowników z długim czasem uruchamiania powłoki, ta wartość może wymagać zwiększenia, jeśli widzisz błędy \"Shell Integration Unavailable\" w terminalu. <0>Dowiedz się więcej" diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 532f53b502..4167ade974 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -508,6 +508,10 @@ "label": "Limite de saída do terminal", "description": "Número máximo de linhas a incluir na saída do terminal ao executar comandos. Quando excedido, as linhas serão removidas do meio, economizando token. <0>Saiba mais" }, + "outputCharacterLimit": { + "label": "Limite de caracteres do terminal", + "description": "Número máximo de caracteres a serem incluídos na saída do terminal ao executar comandos. Este limite tem precedência sobre o limite de linhas para evitar problemas de memória com linhas extremamente longas. Quando excedido, a saída será truncada. <0>Saiba mais" + }, "shellIntegrationTimeout": { "label": "Tempo limite de integração do shell do terminal", "description": "Tempo máximo de espera para a inicialização da integração do shell antes de executar comandos. Para usuários com tempos de inicialização de shell longos, este valor pode precisar ser aumentado se você vir erros \"Shell Integration Unavailable\" no terminal. <0>Saiba mais" diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index f6b0024859..c07bc1d98a 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -508,6 +508,10 @@ "label": "Лимит вывода терминала", "description": "Максимальное количество строк, включаемых в вывод терминала при выполнении команд. При превышении строки из середины будут удаляться для экономии токенов. <0>Подробнее" }, + "outputCharacterLimit": { + "label": "Лимит символов терминала", + "description": "Максимальное количество символов для включения в вывод терминала при выполнении команд. Этот лимит имеет приоритет над лимитом строк, чтобы предотвратить проблемы с памятью из-за чрезвычайно длинных строк. При превышении лимита вывод будет усечен. <0>Узнать больше" + }, "shellIntegrationTimeout": { "label": "Таймаут интеграции оболочки терминала", "description": "Максимальное время ожидания инициализации интеграции оболочки перед выполнением команд. Для пользователей с долгим стартом shell это значение можно увеличить, если появляются ошибки \"Shell Integration Unavailable\". <0>Подробнее" diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index de7f398fd4..ae6fad364d 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -508,6 +508,10 @@ "label": "Terminal çıktısı sınırı", "description": "Komutları yürütürken terminal çıktısına dahil edilecek maksimum satır sayısı. Aşıldığında, token tasarrufu sağlayarak satırlar ortadan kaldırılacaktır. <0>Daha fazla bilgi" }, + "outputCharacterLimit": { + "label": "Terminal karakter sınırı", + "description": "Komutları yürütürken terminal çıktısına dahil edilecek maksimum karakter sayısı. Bu sınır, aşırı uzun satırlardan kaynaklanan bellek sorunlarını önlemek için satır sınırına göre önceliklidir. Aşıldığında, çıktı kesilir. <0>Daha fazla bilgi edinin" + }, "shellIntegrationTimeout": { "label": "Terminal kabuk entegrasyonu zaman aşımı", "description": "Komutları yürütmeden önce kabuk entegrasyonunun başlatılması için beklenecek maksimum süre. Kabuk başlatma süresi uzun olan kullanıcılar için, terminalde \"Shell Integration Unavailable\" hatalarını görürseniz bu değerin artırılması gerekebilir. <0>Daha fazla bilgi" diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index c57289e588..6505d7df0e 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -508,6 +508,10 @@ "label": "Giới hạn đầu ra terminal", "description": "Số dòng tối đa để đưa vào đầu ra terminal khi thực hiện lệnh. Khi vượt quá, các dòng sẽ bị xóa khỏi phần giữa, tiết kiệm token. <0>Tìm hiểu thêm" }, + "outputCharacterLimit": { + "label": "Giới hạn ký tự của terminal", + "description": "Số ký tự tối đa để bao gồm trong đầu ra của terminal khi thực thi lệnh. Giới hạn này được ưu tiên hơn giới hạn dòng để ngăn chặn các vấn đề về bộ nhớ do các dòng quá dài. Khi vượt quá, đầu ra sẽ bị cắt bớt. <0>Tìm hiểu thêm" + }, "shellIntegrationTimeout": { "label": "Thời gian chờ tích hợp shell terminal", "description": "Thời gian tối đa để chờ tích hợp shell khởi tạo trước khi thực hiện lệnh. Đối với người dùng có thời gian khởi động shell dài, giá trị này có thể cần được tăng lên nếu bạn thấy lỗi \"Shell Integration Unavailable\" trong terminal. <0>Tìm hiểu thêm" diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 128ae29378..bf3eea0294 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -508,6 +508,10 @@ "label": "终端输出限制", "description": "执行命令时在终端输出中包含的最大行数。超过时将从中间删除行,节省 token。 <0>了解更多" }, + "outputCharacterLimit": { + "label": "终端字符限制", + "description": "执行命令时在终端输出中包含的最大字符数。此限制优先于行数限制,以防止因行过长而导致的内存问题。超出后,输出将被截断。 <0>了解更多" + }, "shellIntegrationTimeout": { "label": "终端初始化等待时间", "description": "执行命令前等待 Shell 集成初始化的最长时间。对于 Shell 启动时间较长的用户,如果在终端中看到\"Shell Integration Unavailable\"错误,可能需要增加此值。 <0>了解更多" diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 0d3f6d7244..e2a896dce4 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -508,6 +508,10 @@ "label": "終端機輸出行數限制", "description": "執行命令時終端機輸出的最大行數。超過此限制時,會從中間移除多餘的行數,以節省 token 用量。 <0>瞭解更多" }, + "outputCharacterLimit": { + "label": "終端機字元限制", + "description": "執行指令時在終端輸出中包含的最大字元數。此限制優先於行數限制,以防止因行過長而導致的記憶體問題。超過後,輸出將被截斷。 <0>了解更多" + }, "shellIntegrationTimeout": { "label": "終端機 Shell 整合逾時", "description": "執行命令前等待 Shell 整合初始化的最長時間。如果您的 Shell 啟動較慢,且終端機出現「Shell 整合無法使用」的錯誤訊息,可能需要提高此數值。 <0>瞭解更多" From 1b1210817251a2206987e2e3ca5222cd37e0c1c0 Mon Sep 17 00:00:00 2001 From: Murilo Pires <50873657+MuriloFP@users.noreply.github.com> Date: Sat, 19 Jul 2025 20:25:17 -0300 Subject: [PATCH 13/27] fix: enable export, share, and copy buttons during API operations (#5324) (#5849) * feat: add Issue Fixer Orchestrator mode * fix: allow export task history while API is active (#5324) - Add exportAlwaysEnabled prop to TaskActions component - Export button remains enabled when exportAlwaysEnabled is true - Other action buttons still respect buttonsDisabled state - Add tests to verify the new behavior This fixes the regression where users couldn't export task history during API operations, which is a common debugging workflow. * fix: simplify export button to always be enabled The export functionality is not impacted by the model streaming state, so the button should always be enabled. Removed the unnecessary exportAlwaysEnabled prop and simplified the implementation. - Remove exportAlwaysEnabled prop from TaskActions - Remove disabled attribute from export button entirely - Update TaskHeader to remove exportAlwaysEnabled prop usage - Update tests to reflect that export is always enabled * fix: enable export, share, and copy buttons during API operations - Export, share, and copy buttons now remain enabled when API is active - Delete button still respects buttonsDisabled state for safety - Removed unnecessary exportAlwaysEnabled prop - Updated tests to reflect new behavior --------- Co-authored-by: Daniel Riccio --- .../src/components/chat/TaskActions.tsx | 4 +- .../chat/__tests__/TaskActions.spec.tsx | 46 +++++++++++++++++-- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/webview-ui/src/components/chat/TaskActions.tsx b/webview-ui/src/components/chat/TaskActions.tsx index 01340de475..603b6be3e0 100644 --- a/webview-ui/src/components/chat/TaskActions.tsx +++ b/webview-ui/src/components/chat/TaskActions.tsx @@ -23,18 +23,16 @@ export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => { return (
- + vscode.postMessage({ type: "exportCurrentTask" })} /> {item?.task && ( copyWithFeedback(item.task, e)} /> )} diff --git a/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx b/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx index 5db6287747..68c564f823 100644 --- a/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/TaskActions.spec.tsx @@ -371,16 +371,54 @@ describe("TaskActions", () => { }) describe("Button States", () => { - it("disables buttons when buttonsDisabled is true", () => { + it("keeps share, export, and copy buttons enabled but disables delete button when buttonsDisabled is true", () => { render() - // Find button by its icon class + // Find buttons by their labels/icons const buttons = screen.getAllByRole("button") const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) const exportButton = screen.getByLabelText("Export task history") + const copyButton = buttons.find((btn) => btn.querySelector(".codicon-copy")) + const deleteButton = screen.getByLabelText("Delete Task (Shift + Click to skip confirmation)") - expect(shareButton).toBeDisabled() - expect(exportButton).toBeDisabled() + // Share, export, and copy buttons should be enabled regardless of buttonsDisabled + expect(shareButton).not.toBeDisabled() + expect(exportButton).not.toBeDisabled() + expect(copyButton).not.toBeDisabled() + // Delete button should respect buttonsDisabled + expect(deleteButton).toBeDisabled() + }) + + it("share, export, and copy buttons are always enabled while delete button respects buttonsDisabled state", () => { + // Test with buttonsDisabled = false + const { rerender } = render() + + let buttons = screen.getAllByRole("button") + let shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + let exportButton = screen.getByLabelText("Export task history") + let copyButton = buttons.find((btn) => btn.querySelector(".codicon-copy")) + let deleteButton = screen.getByLabelText("Delete Task (Shift + Click to skip confirmation)") + + expect(shareButton).not.toBeDisabled() + expect(exportButton).not.toBeDisabled() + expect(copyButton).not.toBeDisabled() + expect(deleteButton).not.toBeDisabled() + + // Test with buttonsDisabled = true + rerender() + + buttons = screen.getAllByRole("button") + shareButton = buttons.find((btn) => btn.querySelector(".codicon-link")) + exportButton = screen.getByLabelText("Export task history") + copyButton = buttons.find((btn) => btn.querySelector(".codicon-copy")) + deleteButton = screen.getByLabelText("Delete Task (Shift + Click to skip confirmation)") + + // Share, export, and copy remain enabled + expect(shareButton).not.toBeDisabled() + expect(exportButton).not.toBeDisabled() + expect(copyButton).not.toBeDisabled() + // Delete button is disabled + expect(deleteButton).toBeDisabled() }) }) }) From c96b399dd79d3f54577c2b44895c23dafa969d02 Mon Sep 17 00:00:00 2001 From: "roomote[bot]" <219738659+roomote[bot]@users.noreply.github.com> Date: Sat, 19 Jul 2025 22:20:23 -0400 Subject: [PATCH 14/27] feat: add configurable timeout for evals (5-10 min) (#5865) * feat: add configurable timeout for evals (5-10 min) - Add timeout field to CreateRun schema with min 5, max 10, default 5 - Add timeout slider UI component to /runs/new page - Update database schema to include timeout column in runs table - Create migration to add timeout column with default value of 5 - Update runTask.ts to use configurable timeout from run settings - Pass timeout parameter through the createRun action * fix: remove unused EVALS_TIMEOUT import * fix: add timeout field to createRun calls in copyRun test - Added timeout: 5 to both createRun calls in copyRun.spec.ts - This fixes the test failure caused by the new required timeout field in the runs schema - The timeout field was added in the configurable timeout feature but the test was not updated * fix: use configurable timeout for Redis key expiration in registerRunner - Updated registerRunner function to accept timeoutSeconds parameter - Modified call in runTask.ts to pass configurable timeout instead of hardcoded EVALS_TIMEOUT - Removed unused EVALS_TIMEOUT import from redis.ts - Ensures Redis keys remain valid for the entire duration of task execution (up to 10 minutes) --------- Co-authored-by: Roo Code Co-authored-by: hannesrudolph --- apps/web-evals/src/actions/runs.ts | 3 ++- apps/web-evals/src/app/runs/new/new-run.tsx | 27 +++++++++++++++++++ apps/web-evals/src/lib/schemas.ts | 5 ++++ packages/evals/src/cli/redis.ts | 14 +++++++--- packages/evals/src/cli/runTask.ts | 14 +++------- .../migrations/0001_add_timeout_to_runs.sql | 1 + .../src/db/queries/__tests__/copyRun.spec.ts | 3 ++- packages/evals/src/db/schema.ts | 1 + 8 files changed, 52 insertions(+), 16 deletions(-) create mode 100644 packages/evals/src/db/migrations/0001_add_timeout_to_runs.sql diff --git a/apps/web-evals/src/actions/runs.ts b/apps/web-evals/src/actions/runs.ts index 90387d3257..be4664d4d3 100644 --- a/apps/web-evals/src/actions/runs.ts +++ b/apps/web-evals/src/actions/runs.ts @@ -22,9 +22,10 @@ import { CreateRun } from "@/lib/schemas" const EVALS_REPO_PATH = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../../../evals") // eslint-disable-next-line @typescript-eslint/no-unused-vars -export async function createRun({ suite, exercises = [], systemPrompt, ...values }: CreateRun) { +export async function createRun({ suite, exercises = [], systemPrompt, timeout, ...values }: CreateRun) { const run = await _createRun({ ...values, + timeout, socketPath: "", // TODO: Get rid of this. }) diff --git a/apps/web-evals/src/app/runs/new/new-run.tsx b/apps/web-evals/src/app/runs/new/new-run.tsx index 444086bd59..90717d6fec 100644 --- a/apps/web-evals/src/app/runs/new/new-run.tsx +++ b/apps/web-evals/src/app/runs/new/new-run.tsx @@ -21,6 +21,9 @@ import { CONCURRENCY_MIN, CONCURRENCY_MAX, CONCURRENCY_DEFAULT, + TIMEOUT_MIN, + TIMEOUT_MAX, + TIMEOUT_DEFAULT, } from "@/lib/schemas" import { cn } from "@/lib/utils" import { useOpenRouterModels } from "@/hooks/use-open-router-models" @@ -77,6 +80,7 @@ export function NewRun() { exercises: [], settings: undefined, concurrency: CONCURRENCY_DEFAULT, + timeout: TIMEOUT_DEFAULT, }, }) @@ -341,6 +345,29 @@ export function NewRun() { )} /> + ( + + Timeout (minutes) + +
+ field.onChange(value[0])} + /> +
{field.value} min
+
+
+ +
+ )} + /> + data.suite === "full" || (data.exercises || []).length > 0, { diff --git a/packages/evals/src/cli/redis.ts b/packages/evals/src/cli/redis.ts index 8f2c164e49..7e6fa77da5 100644 --- a/packages/evals/src/cli/redis.ts +++ b/packages/evals/src/cli/redis.ts @@ -1,7 +1,5 @@ import { createClient, type RedisClientType } from "redis" -import { EVALS_TIMEOUT } from "@roo-code/types" - let redis: RedisClientType | undefined export const redisClient = async () => { @@ -18,11 +16,19 @@ export const getPubSubKey = (runId: number) => `evals:${runId}` export const getRunnersKey = (runId: number) => `runners:${runId}` export const getHeartbeatKey = (runId: number) => `heartbeat:${runId}` -export const registerRunner = async ({ runId, taskId }: { runId: number; taskId: number }) => { +export const registerRunner = async ({ + runId, + taskId, + timeoutSeconds, +}: { + runId: number + taskId: number + timeoutSeconds: number +}) => { const redis = await redisClient() const runnersKey = getRunnersKey(runId) await redis.sAdd(runnersKey, `task-${taskId}:${process.env.HOSTNAME ?? process.pid}`) - await redis.expire(runnersKey, EVALS_TIMEOUT / 1_000) + await redis.expire(runnersKey, timeoutSeconds) } export const deregisterRunner = async ({ runId, taskId }: { runId: number; taskId: number }) => { diff --git a/packages/evals/src/cli/runTask.ts b/packages/evals/src/cli/runTask.ts index 507d614ea5..0683cd7238 100644 --- a/packages/evals/src/cli/runTask.ts +++ b/packages/evals/src/cli/runTask.ts @@ -5,14 +5,7 @@ import * as os from "node:os" import pWaitFor from "p-wait-for" import { execa } from "execa" -import { - type TaskEvent, - TaskCommandName, - RooCodeEventName, - IpcMessageType, - EVALS_SETTINGS, - EVALS_TIMEOUT, -} from "@roo-code/types" +import { type TaskEvent, TaskCommandName, RooCodeEventName, IpcMessageType, EVALS_SETTINGS } from "@roo-code/types" import { IpcClient } from "@roo-code/ipc" import { @@ -42,7 +35,7 @@ export const processTask = async ({ taskId, logger }: { taskId: number; logger?: const task = await findTask(taskId) const { language, exercise } = task const run = await findRun(task.runId) - await registerRunner({ runId: run.id, taskId }) + await registerRunner({ runId: run.id, taskId, timeoutSeconds: (run.timeout || 5) * 60 }) const containerized = isDockerContainer() @@ -304,9 +297,10 @@ export const runTask = async ({ run, task, publish, logger }: RunTaskOptions) => }) try { + const timeoutMs = (run.timeout || 5) * 60 * 1_000 // Convert minutes to milliseconds await pWaitFor(() => !!taskFinishedAt || !!taskAbortedAt || isClientDisconnected, { interval: 1_000, - timeout: EVALS_TIMEOUT, + timeout: timeoutMs, }) } catch (_error) { taskTimedOut = true diff --git a/packages/evals/src/db/migrations/0001_add_timeout_to_runs.sql b/packages/evals/src/db/migrations/0001_add_timeout_to_runs.sql new file mode 100644 index 0000000000..16d3cc1bdd --- /dev/null +++ b/packages/evals/src/db/migrations/0001_add_timeout_to_runs.sql @@ -0,0 +1 @@ +ALTER TABLE "runs" ADD COLUMN "timeout" integer DEFAULT 5 NOT NULL; \ No newline at end of file diff --git a/packages/evals/src/db/queries/__tests__/copyRun.spec.ts b/packages/evals/src/db/queries/__tests__/copyRun.spec.ts index c693e471db..079373d568 100644 --- a/packages/evals/src/db/queries/__tests__/copyRun.spec.ts +++ b/packages/evals/src/db/queries/__tests__/copyRun.spec.ts @@ -23,6 +23,7 @@ describe("copyRun", () => { socketPath: "/tmp/roo.sock", description: "Test run for copying", concurrency: 4, + timeout: 5, }) sourceRunId = run.id @@ -271,7 +272,7 @@ describe("copyRun", () => { }) it("should copy run without task metrics", async () => { - const minimalRun = await createRun({ model: "gpt-3.5-turbo", socketPath: "/tmp/minimal.sock" }) + const minimalRun = await createRun({ model: "gpt-3.5-turbo", socketPath: "/tmp/minimal.sock", timeout: 5 }) const newRunId = await copyRun({ sourceDb: db, targetDb: db, runId: minimalRun.id }) diff --git a/packages/evals/src/db/schema.ts b/packages/evals/src/db/schema.ts index 0338b812e2..73705ac054 100644 --- a/packages/evals/src/db/schema.ts +++ b/packages/evals/src/db/schema.ts @@ -18,6 +18,7 @@ export const runs = pgTable("runs", { pid: integer(), socketPath: text("socket_path").notNull(), concurrency: integer().default(2).notNull(), + timeout: integer().default(5).notNull(), passed: integer().default(0).notNull(), failed: integer().default(0).notNull(), createdAt: timestamp("created_at").notNull(), From de13d8a5cf612d9f88f6d40e6d0b3370bf860fe7 Mon Sep 17 00:00:00 2001 From: SannidhyaSah Date: Sun, 20 Jul 2025 07:54:08 +0530 Subject: [PATCH 15/27] fix: move context condensing prompt to Prompts section (#4924) (#5279) * feat: move context condensing prompt from context to prompts page * fix: remove unused imports after lint fixes --- src/core/webview/webviewMessageHandler.ts | 5 + src/shared/support-prompt.ts | 40 +++++ .../settings/ContextManagementSettings.tsx | 135 +---------------- .../components/settings/PromptsSettings.tsx | 125 +++++++++++----- .../src/components/settings/SettingsView.tsx | 2 - .../ContextManagementSettings.spec.tsx | 140 +----------------- .../settings/__tests__/SettingsView.spec.tsx | 10 ++ webview-ui/src/i18n/locales/ca/prompts.json | 9 ++ webview-ui/src/i18n/locales/de/prompts.json | 9 ++ webview-ui/src/i18n/locales/en/prompts.json | 9 ++ webview-ui/src/i18n/locales/es/prompts.json | 9 ++ webview-ui/src/i18n/locales/fr/prompts.json | 9 ++ webview-ui/src/i18n/locales/hi/prompts.json | 9 ++ webview-ui/src/i18n/locales/id/prompts.json | 9 ++ webview-ui/src/i18n/locales/it/prompts.json | 9 ++ webview-ui/src/i18n/locales/ja/prompts.json | 9 ++ webview-ui/src/i18n/locales/ko/prompts.json | 9 ++ webview-ui/src/i18n/locales/nl/prompts.json | 9 ++ webview-ui/src/i18n/locales/pl/prompts.json | 9 ++ .../src/i18n/locales/pt-BR/prompts.json | 9 ++ webview-ui/src/i18n/locales/ru/prompts.json | 9 ++ webview-ui/src/i18n/locales/tr/prompts.json | 9 ++ webview-ui/src/i18n/locales/vi/prompts.json | 9 ++ .../src/i18n/locales/zh-CN/prompts.json | 9 ++ .../src/i18n/locales/zh-TW/prompts.json | 9 ++ 25 files changed, 317 insertions(+), 302 deletions(-) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index c5a8573319..780d40df89 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1282,6 +1282,11 @@ export const webviewMessageHandler = async ( await provider.postStateToWebview() break case "updateCondensingPrompt": + // Store the condensing prompt in customSupportPrompts["CONDENSE"] instead of customCondensingPrompt + const currentSupportPrompts = getGlobalState("customSupportPrompts") ?? {} + const updatedSupportPrompts = { ...currentSupportPrompts, CONDENSE: message.text } + await updateGlobalState("customSupportPrompts", updatedSupportPrompts) + // Also update the old field for backward compatibility during migration await updateGlobalState("customCondensingPrompt", message.text) await provider.postStateToWebview() break diff --git a/src/shared/support-prompt.ts b/src/shared/support-prompt.ts index 1767a20753..51f4310fc2 100644 --- a/src/shared/support-prompt.ts +++ b/src/shared/support-prompt.ts @@ -35,6 +35,7 @@ interface SupportPromptConfig { type SupportPromptType = | "ENHANCE" + | "CONDENSE" | "EXPLAIN" | "FIX" | "IMPROVE" @@ -49,6 +50,45 @@ const supportPromptConfigs: Record = { template: `Generate an enhanced version of this prompt (reply with only the enhanced prompt - no conversation, explanations, lead-in, bullet points, placeholders, or surrounding quotes): \${userInput}`, + }, + CONDENSE: { + template: `Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions. +This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing with the conversation and supporting any continuing tasks. + +Your summary should be structured as follows: +Context: The context to continue the conversation with. If applicable based on the current task, this should include: + 1. Previous Conversation: High level details about what was discussed throughout the entire conversation with the user. This should be written to allow someone to be able to follow the general overarching conversation flow. + 2. Current Work: Describe in detail what was being worked on prior to this request to summarize the conversation. Pay special attention to the more recent messages in the conversation. + 3. Key Technical Concepts: List all important technical concepts, technologies, coding conventions, and frameworks discussed, which might be relevant for continuing with this work. + 4. Relevant Files and Code: If applicable, enumerate specific files and code sections examined, modified, or created for the task continuation. Pay special attention to the most recent messages and changes. + 5. Problem Solving: Document problems solved thus far and any ongoing troubleshooting efforts. + 6. Pending Tasks and Next Steps: Outline all pending tasks that you have explicitly been asked to work on, as well as list the next steps you will take for all outstanding work, if applicable. Include code snippets where they add clarity. For any next steps, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no information loss in context between tasks. + +Example summary structure: +1. Previous Conversation: + [Detailed description] +2. Current Work: + [Detailed description] +3. Key Technical Concepts: + - [Concept 1] + - [Concept 2] + - [...] +4. Relevant Files and Code: + - [File Name 1] + - [Summary of why this file is important] + - [Summary of the changes made to this file, if any] + - [Important Code Snippet] + - [File Name 2] + - [Important Code Snippet] + - [...] +5. Problem Solving: + [Detailed description] +6. Pending Tasks and Next Steps: + - [Task 1 details & next steps] + - [Task 2 details & next steps] + - [...] + +Output only the summary of the conversation so far, without any additional commentary or explanation.`, }, EXPLAIN: { template: `Explain the following code from file path \${filePath}:\${startLine}-\${endLine} diff --git a/webview-ui/src/components/settings/ContextManagementSettings.tsx b/webview-ui/src/components/settings/ContextManagementSettings.tsx index 6fbd2f1dab..4afcc6f7b4 100644 --- a/webview-ui/src/components/settings/ContextManagementSettings.tsx +++ b/webview-ui/src/components/settings/ContextManagementSettings.tsx @@ -1,62 +1,20 @@ import { HTMLAttributes } from "react" import React from "react" import { useAppTranslation } from "@/i18n/TranslationContext" -import { VSCodeCheckbox, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react" +import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react" import { Database, FoldVertical } from "lucide-react" import { cn } from "@/lib/utils" -import { Button, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Slider } from "@/components/ui" +import { Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Slider } from "@/components/ui" import { SetCachedStateField } from "./types" import { SectionHeader } from "./SectionHeader" import { Section } from "./Section" import { vscode } from "@/utils/vscode" -const SUMMARY_PROMPT = `\ -Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions. -This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing with the conversation and supporting any continuing tasks. - -Your summary should be structured as follows: -Context: The context to continue the conversation with. If applicable based on the current task, this should include: - 1. Previous Conversation: High level details about what was discussed throughout the entire conversation with the user. This should be written to allow someone to be able to follow the general overarching conversation flow. - 2. Current Work: Describe in detail what was being worked on prior to this request to summarize the conversation. Pay special attention to the more recent messages in the conversation. - 3. Key Technical Concepts: List all important technical concepts, technologies, coding conventions, and frameworks discussed, which might be relevant for continuing with this work. - 4. Relevant Files and Code: If applicable, enumerate specific files and code sections examined, modified, or created for the task continuation. Pay special attention to the most recent messages and changes. - 5. Problem Solving: Document problems solved thus far and any ongoing troubleshooting efforts. - 6. Pending Tasks and Next Steps: Outline all pending tasks that you have explicitly been asked to work on, as well as list the next steps you will take for all outstanding work, if applicable. Include code snippets where they add clarity. For any next steps, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no information loss in context between tasks. - -Example summary structure: -1. Previous Conversation: - [Detailed description] -2. Current Work: - [Detailed description] -3. Key Technical Concepts: - - [Concept 1] - - [Concept 2] - - [...] -4. Relevant Files and Code: - - [File Name 1] - - [Summary of why this file is important] - - [Summary of the changes made to this file, if any] - - [Important Code Snippet] - - [File Name 2] - - [Important Code Snippet] - - [...] -5. Problem Solving: - [Detailed description] -6. Pending Tasks and Next Steps: - - [Task 1 details & next steps] - - [Task 2 details & next steps] - - [...] - -Output only the summary of the conversation so far, without any additional commentary or explanation. -` - type ContextManagementSettingsProps = HTMLAttributes & { autoCondenseContext: boolean autoCondenseContextPercent: number - condensingApiConfigId?: string - customCondensingPrompt?: string listApiConfigMeta: any[] maxOpenTabsContext: number maxWorkspaceFiles: number @@ -67,8 +25,6 @@ type ContextManagementSettingsProps = HTMLAttributes & { setCachedStateField: SetCachedStateField< | "autoCondenseContext" | "autoCondenseContextPercent" - | "condensingApiConfigId" - | "customCondensingPrompt" | "maxOpenTabsContext" | "maxWorkspaceFiles" | "showRooIgnoredFiles" @@ -81,8 +37,6 @@ type ContextManagementSettingsProps = HTMLAttributes & { export const ContextManagementSettings = ({ autoCondenseContext, autoCondenseContextPercent, - condensingApiConfigId, - customCondensingPrompt, listApiConfigMeta, maxOpenTabsContext, maxWorkspaceFiles, @@ -321,91 +275,6 @@ export const ContextManagementSettings = ({ : t("settings:contextManagement.condensingThreshold.profileDescription")}
- - {/* API Configuration Selection */} -
-
- -
{t("settings:contextManagement.condensingApiConfiguration.label")}
-
-
-
- {t("settings:contextManagement.condensingApiConfiguration.description")} -
- -
-
- - {/* Custom Prompt Section */} -
-
- -
{t("settings:contextManagement.customCondensingPrompt.label")}
-
-
-
- {t("settings:contextManagement.customCondensingPrompt.description")} -
- { - const value = (e.target as HTMLTextAreaElement).value - setCachedStateField("customCondensingPrompt", value) - vscode.postMessage({ - type: "updateCondensingPrompt", - text: value, - }) - }} - rows={8} - className="w-full font-mono text-sm" - /> -
- -
-
-
)} diff --git a/webview-ui/src/components/settings/PromptsSettings.tsx b/webview-ui/src/components/settings/PromptsSettings.tsx index 160f79dc84..a71132d62b 100644 --- a/webview-ui/src/components/settings/PromptsSettings.tsx +++ b/webview-ui/src/components/settings/PromptsSettings.tsx @@ -26,7 +26,15 @@ interface PromptsSettingsProps { const PromptsSettings = ({ customSupportPrompts, setCustomSupportPrompts }: PromptsSettingsProps) => { const { t } = useAppTranslation() - const { listApiConfigMeta, enhancementApiConfigId, setEnhancementApiConfigId } = useExtensionState() + const { + listApiConfigMeta, + enhancementApiConfigId, + setEnhancementApiConfigId, + condensingApiConfigId, + setCondensingApiConfigId, + customCondensingPrompt, + setCustomCondensingPrompt, + } = useExtensionState() const [testPrompt, setTestPrompt] = useState("") const [isEnhancing, setIsEnhancing] = useState(false) @@ -48,17 +56,36 @@ const PromptsSettings = ({ customSupportPrompts, setCustomSupportPrompts }: Prom }, []) const updateSupportPrompt = (type: SupportPromptType, value: string | undefined) => { - const updatedPrompts = { ...customSupportPrompts, [type]: value } - setCustomSupportPrompts(updatedPrompts) + if (type === "CONDENSE") { + setCustomCondensingPrompt(value || supportPrompt.default.CONDENSE) + vscode.postMessage({ + type: "updateCondensingPrompt", + text: value || supportPrompt.default.CONDENSE, + }) + } else { + const updatedPrompts = { ...customSupportPrompts, [type]: value } + setCustomSupportPrompts(updatedPrompts) + } } const handleSupportReset = (type: SupportPromptType) => { - const updatedPrompts = { ...customSupportPrompts } - delete updatedPrompts[type] - setCustomSupportPrompts(updatedPrompts) + if (type === "CONDENSE") { + setCustomCondensingPrompt(supportPrompt.default.CONDENSE) + vscode.postMessage({ + type: "updateCondensingPrompt", + text: supportPrompt.default.CONDENSE, + }) + } else { + const updatedPrompts = { ...customSupportPrompts } + delete updatedPrompts[type] + setCustomSupportPrompts(updatedPrompts) + } } const getSupportPromptValue = (type: SupportPromptType): string => { + if (type === "CONDENSE") { + return customCondensingPrompt || supportPrompt.default.CONDENSE + } return supportPrompt.get(customSupportPrompts, type) } @@ -129,29 +156,50 @@ const PromptsSettings = ({ customSupportPrompts, setCustomSupportPrompts }: Prom className="w-full" /> - {activeSupportOption === "ENHANCE" && ( + {(activeSupportOption === "ENHANCE" || activeSupportOption === "CONDENSE") && (
- {t("prompts:supportPrompts.enhance.apiConfigDescription")} + {activeSupportOption === "ENHANCE" + ? t("prompts:supportPrompts.enhance.apiConfigDescription") + : t("prompts:supportPrompts.condense.apiConfigDescription")}
-
- - setTestPrompt((e.target as HTMLTextAreaElement).value)} - placeholder={t("prompts:supportPrompts.enhance.testPromptPlaceholder")} - rows={3} - className="w-full" - data-testid="test-prompt-textarea" - /> -
- + {activeSupportOption === "ENHANCE" && ( +
+ + setTestPrompt((e.target as HTMLTextAreaElement).value)} + placeholder={t("prompts:supportPrompts.enhance.testPromptPlaceholder")} + rows={3} + className="w-full" + data-testid="test-prompt-textarea" + /> +
+ +
-
+ )}
)}
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 882da54ab4..517c1c159d 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -659,8 +659,6 @@ const SettingsView = forwardRef(({ onDone, t ({ })) // Mock vscode utilities - this is necessary since we're not in a VSCode environment -import { vscode } from "@/utils/vscode" vitest.mock("@/utils/vscode", () => ({ vscode: { @@ -169,8 +168,6 @@ describe("ContextManagementSettings", () => { ...defaultProps, autoCondenseContext: true, autoCondenseContextPercent: 75, - condensingApiConfigId: "test-config", - customCondensingPrompt: "Test prompt", } render() @@ -182,13 +179,9 @@ describe("ContextManagementSettings", () => { const slider = screen.getByTestId("condense-threshold-slider") expect(slider).toBeInTheDocument() - // Should render both select dropdowns (profile and API config) + // Should render the profile select dropdown const selects = screen.getAllByRole("combobox") - expect(selects).toHaveLength(2) - - // Should render the custom prompt textarea - const textarea = screen.getByRole("textbox") - expect(textarea).toBeInTheDocument() + expect(selects).toHaveLength(1) }) describe("Auto Condense Context functionality", () => { @@ -196,8 +189,6 @@ describe("ContextManagementSettings", () => { ...defaultProps, autoCondenseContext: true, autoCondenseContextPercent: 75, - condensingApiConfigId: "test-config", - customCondensingPrompt: "Custom test prompt", listApiConfigMeta: [ { id: "config-1", name: "Config 1" }, { id: "config-2", name: "Config 2" }, @@ -217,14 +208,13 @@ describe("ContextManagementSettings", () => { expect(mockSetCachedStateField).toHaveBeenCalledWith("autoCondenseContext", false) }) - it("shows additional settings when auto condense is enabled", () => { + it("shows threshold settings when auto condense is enabled", () => { render() - // Additional settings should be visible + // Threshold settings should be visible expect(screen.getByTestId("condense-threshold-slider")).toBeInTheDocument() - // Two comboboxes: one for profile selection, one for API config - expect(screen.getAllByRole("combobox")).toHaveLength(2) - expect(screen.getByRole("textbox")).toBeInTheDocument() + // One combobox for profile selection + expect(screen.getAllByRole("combobox")).toHaveLength(1) }) it("updates auto condense context percent", () => { @@ -246,112 +236,6 @@ describe("ContextManagementSettings", () => { render() expect(screen.getByText("75%")).toBeInTheDocument() }) - - it("updates condensing API configuration", () => { - const mockSetCachedStateField = vitest.fn() - const mockPostMessage = vitest.fn() - const postMessageSpy = vitest.spyOn(vscode, "postMessage") - postMessageSpy.mockImplementation(mockPostMessage) - - const props = { ...autoCondenseProps, setCachedStateField: mockSetCachedStateField } - render() - - // Get the second combobox (API config select) - const selects = screen.getAllByRole("combobox") - const apiSelect = selects[1] - fireEvent.click(apiSelect) - - const configOption = screen.getByText("Config 1") - fireEvent.click(configOption) - - expect(mockSetCachedStateField).toHaveBeenCalledWith("condensingApiConfigId", "config-1") - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "condensingApiConfigId", - text: "config-1", - }) - }) - - it("handles selecting default config option", () => { - const mockSetCachedStateField = vitest.fn() - const mockPostMessage = vitest.fn() - const postMessageSpy = vitest.spyOn(vscode, "postMessage") - postMessageSpy.mockImplementation(mockPostMessage) - - const props = { ...autoCondenseProps, setCachedStateField: mockSetCachedStateField } - render() - - // Test selecting default config - get the second combobox (API config) - const selects = screen.getAllByRole("combobox") - const apiSelect = selects[1] - fireEvent.click(apiSelect) - const defaultOption = screen.getByText( - "settings:contextManagement.condensingApiConfiguration.useCurrentConfig", - ) - fireEvent.click(defaultOption) - - expect(mockSetCachedStateField).toHaveBeenCalledWith("condensingApiConfigId", "") - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "condensingApiConfigId", - text: "", - }) - }) - - it("updates custom condensing prompt", () => { - const mockSetCachedStateField = vitest.fn() - const mockPostMessage = vitest.fn() - const postMessageSpy = vitest.spyOn(vscode, "postMessage") - postMessageSpy.mockImplementation(mockPostMessage) - - const props = { ...autoCondenseProps, setCachedStateField: mockSetCachedStateField } - render() - - const textarea = screen.getByRole("textbox") - const newPrompt = "Updated custom prompt" - fireEvent.change(textarea, { target: { value: newPrompt } }) - - expect(mockSetCachedStateField).toHaveBeenCalledWith("customCondensingPrompt", newPrompt) - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "updateCondensingPrompt", - text: newPrompt, - }) - }) - - it("resets custom condensing prompt to default", () => { - const mockSetCachedStateField = vitest.fn() - const mockPostMessage = vitest.fn() - const postMessageSpy = vitest.spyOn(vscode, "postMessage") - postMessageSpy.mockImplementation(mockPostMessage) - - const props = { ...autoCondenseProps, setCachedStateField: mockSetCachedStateField } - render() - - const resetButton = screen.getByRole("button", { - name: "settings:contextManagement.customCondensingPrompt.reset", - }) - fireEvent.click(resetButton) - - // Should reset to the default SUMMARY_PROMPT - expect(mockSetCachedStateField).toHaveBeenCalledWith( - "customCondensingPrompt", - expect.stringContaining("Your task is to create a detailed summary"), - ) - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "updateCondensingPrompt", - text: expect.stringContaining("Your task is to create a detailed summary"), - }) - }) - - it("uses default prompt when customCondensingPrompt is undefined", () => { - const propsWithoutCustomPrompt = { - ...autoCondenseProps, - customCondensingPrompt: undefined, - } - render() - - const textarea = screen.getByRole("textbox") as HTMLTextAreaElement - // The textarea should contain the full default SUMMARY_PROMPT - expect(textarea.value).toContain("Your task is to create a detailed summary") - }) }) describe("Edge cases and validation", () => { @@ -427,8 +311,6 @@ describe("ContextManagementSettings", () => { ...defaultProps, showRooIgnoredFiles: undefined, maxReadFileLine: undefined, - condensingApiConfigId: undefined, - customCondensingPrompt: undefined, } expect(() => { @@ -442,21 +324,15 @@ describe("ContextManagementSettings", () => { }) describe("Conditional rendering", () => { - it("does not render auto condense section when autoCondenseContext is false", () => { + it("does not render threshold settings when autoCondenseContext is false", () => { const propsWithoutAutoCondense = { ...defaultProps, autoCondenseContext: false, } render() - // When auto condense is false, all condensing-related UI should not be visible + // When auto condense is false, threshold slider should not be visible expect(screen.queryByTestId("condense-threshold-slider")).not.toBeInTheDocument() - expect( - screen.queryByText("settings:contextManagement.condensingApiConfiguration.label"), - ).not.toBeInTheDocument() - expect( - screen.queryByText("settings:contextManagement.customCondensingPrompt.label"), - ).not.toBeInTheDocument() }) it("renders max read file controls with default value when maxReadFileLine is undefined", () => { diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx index 72d4598ea6..694ff174a7 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx @@ -58,6 +58,16 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({ ), VSCodeRadioGroup: ({ children, onChange }: any) =>
{children}
, + VSCodeTextArea: ({ value, onChange, rows, className, "data-testid": dataTestId }: any) => ( +