From 7114cef03e3cbad99a733fe5b9ab1200cd9b4f26 Mon Sep 17 00:00:00 2001 From: lightrabbit Date: Tue, 11 Mar 2025 14:13:17 +0800 Subject: [PATCH 1/9] feat: openai-compatible deepseek/qwq reasoning support --- src/api/providers/openai.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index a6e3eb1488..ee6137133c 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -14,6 +14,7 @@ import { convertToR1Format } from "../transform/r1-format" import { convertToSimpleMessages } from "../transform/simple-format" import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" import { BaseProvider } from "./base-provider" +import { XmlMatcher } from "../../utils/xml-matcher" const DEEP_SEEK_DEFAULT_TEMPERATURE = 0.6 @@ -99,15 +100,23 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const stream = await this.client.chat.completions.create(requestOptions) + const matcher = new XmlMatcher( + "think", + (chunk) => + ({ + type: chunk.matched ? "reasoning" : "text", + text: chunk.data, + }) as const, + ) + let lastUsage for await (const chunk of stream) { const delta = chunk.choices[0]?.delta ?? {} if (delta.content) { - yield { - type: "text", - text: delta.content, + for (const chunk of matcher.update(delta.content)) { + yield chunk } } @@ -121,6 +130,9 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl lastUsage = chunk.usage } } + for (const chunk of matcher.final()) { + yield chunk + } if (lastUsage) { yield this.processUsageMetrics(lastUsage, modelInfo) From 4b6def5f314a4fce0353f10335dddfaf303f6634 Mon Sep 17 00:00:00 2001 From: cte Date: Tue, 11 Mar 2025 14:24:21 -0700 Subject: [PATCH 2/9] ContextProxy fix - constructor should not be async --- .changeset/seven-apricots-watch.md | 5 + .github/workflows/code-qa.yml | 4 +- .gitignore | 4 +- ....integration.example => .env.local.sample} | 0 e2e/VSCODE_INTEGRATION_TESTS.md | 16 +- e2e/package.json | 9 +- e2e/src/suite/index.ts | 72 ++---- e2e/src/suite/modes.test.ts | 94 +++----- e2e/src/suite/subtasks.test.ts | 58 +++++ e2e/src/suite/task.test.ts | 161 +------------- e2e/src/suite/utils.ts | 76 +++++++ knip.json | 6 +- src/activate/createRooCodeAPI.ts | 41 ++-- src/api/providers/openrouter.ts | 59 +++-- src/core/__tests__/contextProxy.test.ts | 59 ++--- src/core/contextProxy.ts | 137 ++++++------ src/core/prompts/sections/modes.ts | 2 +- src/core/webview/ClineProvider.ts | 29 ++- .../webview/__tests__/ClineProvider.test.ts | 2 + src/exports/README.md | 7 - src/exports/roo-code.d.ts | 210 ++++++++++-------- src/shared/ExtensionMessage.ts | 2 +- src/shared/globalState.ts | 40 +++- 23 files changed, 531 insertions(+), 562 deletions(-) create mode 100644 .changeset/seven-apricots-watch.md rename e2e/{.env.integration.example => .env.local.sample} (100%) create mode 100644 e2e/src/suite/subtasks.test.ts create mode 100644 e2e/src/suite/utils.ts diff --git a/.changeset/seven-apricots-watch.md b/.changeset/seven-apricots-watch.md new file mode 100644 index 0000000000..ec51cccdea --- /dev/null +++ b/.changeset/seven-apricots-watch.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +ContextProxy fix - constructor should not be async diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index 2e269cbd79..2bbad069bb 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -110,9 +110,9 @@ jobs: cache: 'npm' - name: Install dependencies run: npm run install:all - - name: Create env.integration file + - name: Create .env.local file working-directory: e2e - run: echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" > .env.integration + run: echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" > .env.local - name: Run integration tests working-directory: e2e run: xvfb-run -a npm run ci diff --git a/.gitignore b/.gitignore index e5cc9a6117..02fdf8f88e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,9 @@ docs/_site/ # Dotenv .env -.env.integration +.env.* +!.env.*.sample + #Local lint config .eslintrc.local.json diff --git a/e2e/.env.integration.example b/e2e/.env.local.sample similarity index 100% rename from e2e/.env.integration.example rename to e2e/.env.local.sample diff --git a/e2e/VSCODE_INTEGRATION_TESTS.md b/e2e/VSCODE_INTEGRATION_TESTS.md index a36b240381..452c00c226 100644 --- a/e2e/VSCODE_INTEGRATION_TESTS.md +++ b/e2e/VSCODE_INTEGRATION_TESTS.md @@ -30,7 +30,7 @@ The test runner (`runTest.ts`) is responsible for: ### Environment Setup -1. Create a `.env.integration` file in the root directory with required environment variables: +1. Create a `.env.local` file in the root directory with required environment variables: ``` OPENROUTER_API_KEY=sk-or-v1-... @@ -67,7 +67,7 @@ declare global { ## Running Tests -1. Ensure you have the required environment variables set in `.env.integration` +1. Ensure you have the required environment variables set in `.env.local` 2. Run the integration tests: @@ -117,8 +117,10 @@ const interval = 1000 2. **State Management**: Reset extension state before/after tests: ```typescript -await globalThis.provider.updateGlobalState("mode", "Ask") -await globalThis.provider.updateGlobalState("alwaysAllowModeSwitch", true) +await globalThis.api.setConfiguration({ + mode: "Ask", + alwaysAllowModeSwitch: true, +}) ``` 3. **Assertions**: Use clear assertions with meaningful messages: @@ -141,8 +143,12 @@ try { ```typescript let startTime = Date.now() + while (Date.now() - startTime < timeout) { - if (condition) break + if (condition) { + break + } + await new Promise((resolve) => setTimeout(resolve, interval)) } ``` diff --git a/e2e/package.json b/e2e/package.json index 630a13f0e7..d4932f8a07 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -3,12 +3,13 @@ "version": "0.1.0", "private": true, "scripts": { - "build": "cd .. && npm run build", - "compile": "tsc -p tsconfig.json", + "build": "cd .. && npm run compile && npm run build:webview", + "compile": "rm -rf out && tsc -p tsconfig.json", "lint": "eslint src --ext ts", "check-types": "tsc --noEmit", - "test": "npm run compile && npx dotenvx run -f .env.integration -- node ./out/runTest.js", - "ci": "npm run build && npm run test" + "test": "npm run compile && npx dotenvx run -f .env.local -- node ./out/runTest.js", + "ci": "npm run build && npm run test", + "clean": "rimraf out" }, "dependencies": {}, "devDependencies": { diff --git a/e2e/src/suite/index.ts b/e2e/src/suite/index.ts index 19e00aa40c..aea3d88232 100644 --- a/e2e/src/suite/index.ts +++ b/e2e/src/suite/index.ts @@ -1,80 +1,46 @@ import * as path from "path" import Mocha from "mocha" import { glob } from "glob" -import { RooCodeAPI, ClineProvider } from "../../../src/exports/roo-code" import * as vscode from "vscode" +import { RooCodeAPI } from "../../../src/exports/roo-code" + +import { waitUntilReady } from "./utils" + declare global { - var api: RooCodeAPI - var provider: ClineProvider var extension: vscode.Extension | undefined - var panel: vscode.WebviewPanel | undefined + var api: RooCodeAPI } -export async function run(): Promise { - const mocha = new Mocha({ - ui: "tdd", - timeout: 600000, // 10 minutes to compensate for time communicating with LLM while running in GHA. - }) - +export async function run() { + const mocha = new Mocha({ ui: "tdd", timeout: 300_000 }) const testsRoot = path.resolve(__dirname, "..") try { // Find all test files. const files = await glob("**/**.test.js", { cwd: testsRoot }) - - // Add files to the test suite. files.forEach((f: string) => mocha.addFile(path.resolve(testsRoot, f))) - // Set up global extension, api, provider, and panel. - globalThis.extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline") + const extension = vscode.extensions.getExtension("RooVeterinaryInc.roo-cline") - if (!globalThis.extension) { + if (!extension) { throw new Error("Extension not found") } - globalThis.api = globalThis.extension.isActive - ? globalThis.extension.exports - : await globalThis.extension.activate() + const api = extension.isActive ? extension.exports : await extension.activate() - globalThis.provider = globalThis.api.sidebarProvider + await api.setConfiguration({ + apiProvider: "openrouter", + openRouterApiKey: process.env.OPENROUTER_API_KEY!, + openRouterModelId: "anthropic/claude-3.5-sonnet", + }) - await globalThis.provider.updateGlobalState("apiProvider", "openrouter") - await globalThis.provider.updateGlobalState("openRouterModelId", "anthropic/claude-3.5-sonnet") + await waitUntilReady(api) - await globalThis.provider.storeSecret( - "openRouterApiKey", - process.env.OPENROUTER_API_KEY || "sk-or-v1-fake-api-key", - ) + globalThis.api = api + globalThis.extension = extension - globalThis.panel = vscode.window.createWebviewPanel( - "roo-cline.SidebarProvider", - "Roo Code", - vscode.ViewColumn.One, - { - enableScripts: true, - enableCommandUris: true, - retainContextWhenHidden: true, - localResourceRoots: [globalThis.extension?.extensionUri], - }, - ) - - await globalThis.provider.resolveWebviewView(globalThis.panel) - - let startTime = Date.now() - const timeout = 60000 - const interval = 1000 - - while (Date.now() - startTime < timeout) { - if (globalThis.provider.viewLaunched) { - break - } - - await new Promise((resolve) => setTimeout(resolve, interval)) - } - - // Run the mocha test. - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { try { mocha.run((failures: number) => { if (failures > 0) { diff --git a/e2e/src/suite/modes.test.ts b/e2e/src/suite/modes.test.ts index b94e71d110..5083d289f6 100644 --- a/e2e/src/suite/modes.test.ts +++ b/e2e/src/suite/modes.test.ts @@ -1,102 +1,60 @@ import * as assert from "assert" +import { waitForMessage } from "./utils" + suite("Roo Code Modes", () => { test("Should handle switching modes correctly", async function () { - const timeout = 30000 - const interval = 1000 + const timeout = 300_000 + const api = globalThis.api const testPrompt = - "For each mode (Code, Architect, Ask) respond with the mode name and what it specializes in after switching to that mode, do not start with the current mode, be sure to say 'I AM DONE' after the task is complete" + "For each mode (Code, Architect, Ask) respond with the mode name and what it specializes in after switching to that mode, do not start with the current mode, be sure to say 'I AM DONE' after the task is complete." - if (!globalThis.extension) { - assert.fail("Extension not found") - } + await api.setConfiguration({ mode: "Code", alwaysAllowModeSwitch: true, autoApprovalEnabled: true }) + await api.startNewTask(testPrompt) - let startTime = Date.now() + await waitForMessage(api, { include: "I AM DONE", exclude: "be sure to say", timeout }) - // Ensure the webview is launched. - while (Date.now() - startTime < timeout) { - if (globalThis.provider.viewLaunched) { - break - } - - await new Promise((resolve) => setTimeout(resolve, interval)) - } - - await globalThis.provider.updateGlobalState("mode", "Ask") - await globalThis.provider.updateGlobalState("alwaysAllowModeSwitch", true) - await globalThis.provider.updateGlobalState("autoApprovalEnabled", true) - - // Start a new task. - await globalThis.api.startNewTask(testPrompt) - - // Wait for task to appear in history with tokens. - startTime = Date.now() - - while (Date.now() - startTime < timeout) { - const messages = globalThis.provider.messages - - if ( - messages.some( - ({ type, text }) => - type === "say" && text?.includes("I AM DONE") && !text?.includes("be sure to say"), - ) - ) { - break - } - - await new Promise((resolve) => setTimeout(resolve, interval)) - } - - if (globalThis.provider.messages.length === 0) { + if (api.getMessages().length === 0) { assert.fail("No messages received") } // Log the messages to the console. - globalThis.provider.messages.forEach(({ type, text }) => { + api.getMessages().forEach(({ type, text }) => { if (type === "say") { console.log(text) } }) // Start Grading Portion of test to grade the response from 1 to 10. - await globalThis.provider.updateGlobalState("mode", "Ask") - let output = globalThis.provider.messages.map(({ type, text }) => (type === "say" ? text : "")).join("\n") + await api.setConfiguration({ mode: "Ask" }) - await globalThis.api.startNewTask( - `Given this prompt: ${testPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)": ${output} \n Be sure to say 'I AM DONE GRADING' after the task is complete`, + let output = api + .getMessages() + .map(({ type, text }) => (type === "say" ? text : "")) + .join("\n") + + await api.startNewTask( + `Given this prompt: ${testPrompt} grade the response from 1 to 10 in the format of "Grade: (1-10)": ${output}\nBe sure to say 'I AM DONE GRADING' after the task is complete.`, ) - startTime = Date.now() + await waitForMessage(api, { include: "I AM DONE GRADING", exclude: "be sure to say", timeout }) - while (Date.now() - startTime < timeout) { - const messages = globalThis.provider.messages - - if ( - messages.some( - ({ type, text }) => - type === "say" && text?.includes("I AM DONE GRADING") && !text?.includes("be sure to say"), - ) - ) { - break - } - - await new Promise((resolve) => setTimeout(resolve, interval)) - } - - if (globalThis.provider.messages.length === 0) { + if (api.getMessages().length === 0) { assert.fail("No messages received") } - globalThis.provider.messages.forEach(({ type, text }) => { + api.getMessages().forEach(({ type, text }) => { if (type === "say" && text?.includes("Grade:")) { console.log(text) } }) - const gradeMessage = globalThis.provider.messages.find( - ({ type, text }) => type === "say" && !text?.includes("Grade: (1-10)") && text?.includes("Grade:"), - )?.text + const gradeMessage = api + .getMessages() + .find( + ({ type, text }) => type === "say" && !text?.includes("Grade: (1-10)") && text?.includes("Grade:"), + )?.text const gradeMatch = gradeMessage?.match(/Grade: (\d+)/) const gradeNum = gradeMatch ? parseInt(gradeMatch[1]) : undefined diff --git a/e2e/src/suite/subtasks.test.ts b/e2e/src/suite/subtasks.test.ts new file mode 100644 index 0000000000..4a3a65375a --- /dev/null +++ b/e2e/src/suite/subtasks.test.ts @@ -0,0 +1,58 @@ +import * as assert from "assert" + +import { sleep, waitForToolUse, waitForMessage } from "./utils" + +suite("Roo Code Subtasks", () => { + test.skip("Should handle subtask cancellation and resumption correctly", async function () { + const api = globalThis.api + + await api.setConfiguration({ + mode: "Code", + alwaysAllowModeSwitch: true, + alwaysAllowSubtasks: true, + autoApprovalEnabled: true, + }) + + // Start a parent task that will create a subtask. + await api.startNewTask( + "You are the parent task. " + + "Create a subtask by using the new_task tool with the message 'You are the subtask'. " + + "After creating the subtask, wait for it to complete and then respond with 'Parent task resumed'.", + ) + + await waitForToolUse(api, "new_task") + + // Cancel the current task (which should be the subtask). + await api.cancelTask() + + // Check if the parent task is still waiting (not resumed). We need to + // wait a bit to ensure any task resumption would have happened. + await sleep(5_000) + + // The parent task should not have resumed yet, so we shouldn't see + // "Parent task resumed". + assert.ok( + !api.getMessages().some(({ type, text }) => type === "say" && text?.includes("Parent task resumed")), + "Parent task should not have resumed after subtask cancellation.", + ) + + // Start a new task with the same message as the subtask. + await api.startNewTask("You are the subtask") + + // Wait for the subtask to complete. + await waitForMessage(api, { include: "Task complete" }) + + // Verify that the parent task is still not resumed. We need to wait a + // bit to ensure any task resumption would have happened. + await sleep(5_000) + + // The parent task should still not have resumed. + assert.ok( + !api.getMessages().some(({ type, text }) => type === "say" && text?.includes("Parent task resumed")), + "Parent task should not have resumed after subtask completion.", + ) + + // Clean up - cancel all tasks. + await api.cancelTask() + }) +}) diff --git a/e2e/src/suite/task.test.ts b/e2e/src/suite/task.test.ts index e6bab19c41..679a82f550 100644 --- a/e2e/src/suite/task.test.ts +++ b/e2e/src/suite/task.test.ts @@ -1,161 +1,10 @@ -import * as assert from "assert" -import * as vscode from "vscode" +import { waitForMessage } from "./utils" suite("Roo Code Task", () => { test("Should handle prompt and response correctly", async function () { - const timeout = 30000 - const interval = 1000 - - if (!globalThis.extension) { - assert.fail("Extension not found") - } - - // Ensure the webview is launched. - let startTime = Date.now() - - while (Date.now() - startTime < timeout) { - if (globalThis.provider.viewLaunched) { - break - } - - await new Promise((resolve) => setTimeout(resolve, interval)) - } - - await globalThis.provider.updateGlobalState("mode", "Code") - await globalThis.provider.updateGlobalState("alwaysAllowModeSwitch", true) - await globalThis.provider.updateGlobalState("autoApprovalEnabled", true) - - await globalThis.api.startNewTask("Hello world, what is your name? Respond with 'My name is ...'") - - // Wait for task to appear in history with tokens. - startTime = Date.now() - - while (Date.now() - startTime < timeout) { - const messages = globalThis.provider.messages - - if (messages.some(({ type, text }) => type === "say" && text?.includes("My name is Roo"))) { - break - } - - await new Promise((resolve) => setTimeout(resolve, interval)) - } - - if (globalThis.provider.messages.length === 0) { - assert.fail("No messages received") - } - - assert.ok( - globalThis.provider.messages.some(({ type, text }) => type === "say" && text?.includes("My name is Roo")), - "Did not receive expected response containing 'My name is Roo'", - ) - }) - - test("Should handle subtask cancellation and resumption correctly", async function () { - this.timeout(60000) // Increase timeout for this test - const interval = 1000 - - if (!globalThis.extension) { - assert.fail("Extension not found") - } - - // Ensure the webview is launched - await ensureWebviewLaunched(30000, interval) - - // Set up required global state - await globalThis.provider.updateGlobalState("mode", "Code") - await globalThis.provider.updateGlobalState("alwaysAllowModeSwitch", true) - await globalThis.provider.updateGlobalState("alwaysAllowSubtasks", true) - await globalThis.provider.updateGlobalState("autoApprovalEnabled", true) - - // 1. Start a parent task that will create a subtask - await globalThis.api.startNewTask( - "You are the parent task. Create a subtask by using the new_task tool with the message 'You are the subtask'. " + - "After creating the subtask, wait for it to complete and then respond with 'Parent task resumed'.", - ) - - // Wait for the parent task to use the new_task tool - await waitForToolUse("new_task", 30000, interval) - - // Wait for the subtask to be created and start responding - await waitForMessage("You are the subtask", 10000, interval) - - // 3. Cancel the current task (which should be the subtask) - await globalThis.provider.cancelTask() - - // 4. Check if the parent task is still waiting (not resumed) - // We need to wait a bit to ensure any task resumption would have happened - await new Promise((resolve) => setTimeout(resolve, 5000)) - - // The parent task should not have resumed yet, so we shouldn't see "Parent task resumed" - assert.ok( - !globalThis.provider.messages.some( - ({ type, text }) => type === "say" && text?.includes("Parent task resumed"), - ), - "Parent task should not have resumed after subtask cancellation", - ) - - // 5. Start a new task with the same message as the subtask - await globalThis.api.startNewTask("You are the subtask") - - // Wait for the subtask to complete - await waitForMessage("Task complete", 20000, interval) - - // 6. Verify that the parent task is still not resumed - // We need to wait a bit to ensure any task resumption would have happened - await new Promise((resolve) => setTimeout(resolve, 5000)) - - // The parent task should still not have resumed - assert.ok( - !globalThis.provider.messages.some( - ({ type, text }) => type === "say" && text?.includes("Parent task resumed"), - ), - "Parent task should not have resumed after subtask completion", - ) - - // Clean up - cancel all tasks - await globalThis.provider.cancelTask() + const api = globalThis.api + await api.setConfiguration({ mode: "Ask", alwaysAllowModeSwitch: true, autoApprovalEnabled: true }) + await api.startNewTask("Hello world, what is your name? Respond with 'My name is ...'") + await waitForMessage(api, { include: "My name is Roo" }) }) }) - -// Helper functions -async function ensureWebviewLaunched(timeout: number, interval: number): Promise { - const startTime = Date.now() - while (Date.now() - startTime < timeout) { - if (globalThis.provider.viewLaunched) { - return - } - await new Promise((resolve) => setTimeout(resolve, interval)) - } - throw new Error("Webview failed to launch within timeout") -} - -async function waitForToolUse(toolName: string, timeout: number, interval: number): Promise { - const startTime = Date.now() - while (Date.now() - startTime < timeout) { - const messages = globalThis.provider.messages - if ( - messages.some( - (message) => - message.type === "say" && message.say === "tool" && message.text && message.text.includes(toolName), - ) - ) { - return - } - await new Promise((resolve) => setTimeout(resolve, interval)) - } - throw new Error(`Tool ${toolName} was not used within timeout`) -} - -async function waitForMessage(messageContent: string, timeout: number, interval: number): Promise { - const startTime = Date.now() - while (Date.now() - startTime < timeout) { - const messages = globalThis.provider.messages - if ( - messages.some((message) => message.type === "say" && message.text && message.text.includes(messageContent)) - ) { - return - } - await new Promise((resolve) => setTimeout(resolve, interval)) - } - throw new Error(`Message containing "${messageContent}" not found within timeout`) -} diff --git a/e2e/src/suite/utils.ts b/e2e/src/suite/utils.ts new file mode 100644 index 0000000000..c0927fc650 --- /dev/null +++ b/e2e/src/suite/utils.ts @@ -0,0 +1,76 @@ +import * as vscode from "vscode" + +import { RooCodeAPI } from "../../../src/exports/roo-code" + +type WaitForOptions = { + timeout?: number + interval?: number +} + +export const waitFor = ( + condition: (() => Promise) | (() => boolean), + { timeout = 30_000, interval = 250 }: WaitForOptions = {}, +) => { + let timeoutId: NodeJS.Timeout | undefined = undefined + + return Promise.race([ + new Promise((resolve) => { + const check = async () => { + const result = condition() + const isSatisfied = result instanceof Promise ? await result : result + + if (isSatisfied) { + if (timeoutId) { + clearTimeout(timeoutId) + timeoutId = undefined + } + + resolve() + } else { + setTimeout(check, interval) + } + } + + check() + }), + new Promise((_, reject) => { + timeoutId = setTimeout(() => { + reject(new Error(`Timeout after ${Math.floor(timeout / 1000)}s`)) + }, timeout) + }), + ]) +} + +export const waitUntilReady = async (api: RooCodeAPI, { timeout = 10_000, interval = 250 }: WaitForOptions = {}) => { + await vscode.commands.executeCommand("roo-cline.SidebarProvider.focus") + await waitFor(api.isReady, { timeout, interval }) +} + +export const waitForToolUse = async (api: RooCodeAPI, toolName: string, options: WaitForOptions = {}) => + waitFor( + () => + api + .getMessages() + .some(({ type, say, text }) => type === "say" && say === "tool" && text && text.includes(toolName)), + options, + ) + +export const waitForMessage = async ( + api: RooCodeAPI, + options: WaitForOptions & { include: string; exclude?: string }, +) => + waitFor( + () => + api + .getMessages() + .some( + ({ type, text }) => + type === "say" && + text && + text.includes(options.include) && + (!options.exclude || !text.includes(options.exclude)), + ), + options, + ) + +export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) diff --git a/knip.json b/knip.json index a9f0b93e0d..55b60e8210 100644 --- a/knip.json +++ b/knip.json @@ -13,12 +13,10 @@ "dist/**", "out/**", "bin/**", + "e2e/**", "src/activate/**", "src/exports/**", - "src/extension.ts", - "e2e/.vscode-test.mjs", - "e2e/src/runTest.ts", - "e2e/src/suite/index.ts" + "src/extension.ts" ], "workspaces": { "webview-ui": { diff --git a/src/activate/createRooCodeAPI.ts b/src/activate/createRooCodeAPI.ts index 18c696a8fd..aa404999aa 100644 --- a/src/activate/createRooCodeAPI.ts +++ b/src/activate/createRooCodeAPI.ts @@ -3,24 +3,18 @@ import * as vscode from "vscode" import { ClineProvider } from "../core/webview/ClineProvider" import { RooCodeAPI } from "../exports/roo-code" +import { ConfigurationValues } from "../shared/globalState" -export function createRooCodeAPI(outputChannel: vscode.OutputChannel, sidebarProvider: ClineProvider): RooCodeAPI { +export function createRooCodeAPI(outputChannel: vscode.OutputChannel, provider: ClineProvider): RooCodeAPI { return { - setCustomInstructions: async (value: string) => { - await sidebarProvider.updateCustomInstructions(value) - outputChannel.appendLine("Custom instructions set") - }, - - getCustomInstructions: async () => { - return (await sidebarProvider.getGlobalState("customInstructions")) as string | undefined - }, - startNewTask: async (task?: string, images?: string[]) => { outputChannel.appendLine("Starting new task") - await sidebarProvider.removeClineFromStack() - await sidebarProvider.postStateToWebview() - await sidebarProvider.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) - await sidebarProvider.postMessageToWebview({ + + await provider.removeClineFromStack() + await provider.postStateToWebview() + await provider.postMessageToWebview({ type: "action", action: "chatButtonClicked" }) + + await provider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text: task, @@ -32,12 +26,17 @@ export function createRooCodeAPI(outputChannel: vscode.OutputChannel, sidebarPro ) }, + cancelTask: async () => { + outputChannel.appendLine("Cancelling current task") + await provider.cancelTask() + }, + sendMessage: async (message?: string, images?: string[]) => { outputChannel.appendLine( `Sending message: ${message ? `"${message}"` : "undefined"} with ${images?.length || 0} image(s)`, ) - await sidebarProvider.postMessageToWebview({ + await provider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text: message, @@ -47,14 +46,20 @@ export function createRooCodeAPI(outputChannel: vscode.OutputChannel, sidebarPro pressPrimaryButton: async () => { outputChannel.appendLine("Pressing primary button") - await sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "primaryButtonClick" }) + await provider.postMessageToWebview({ type: "invoke", invoke: "primaryButtonClick" }) }, pressSecondaryButton: async () => { outputChannel.appendLine("Pressing secondary button") - await sidebarProvider.postMessageToWebview({ type: "invoke", invoke: "secondaryButtonClick" }) + await provider.postMessageToWebview({ type: "invoke", invoke: "secondaryButtonClick" }) }, - sidebarProvider: sidebarProvider, + setConfiguration: async (values: Partial) => { + await provider.setValues(values) + }, + + isReady: () => provider.viewLaunched, + + getMessages: () => provider.messages, } } diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 01e1393aa5..33feba5db7 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -1,6 +1,6 @@ import { Anthropic } from "@anthropic-ai/sdk" import { BetaThinkingConfigParam } from "@anthropic-ai/sdk/resources/beta" -import axios from "axios" +import axios, { AxiosRequestConfig } from "axios" import OpenAI from "openai" import delay from "delay" @@ -132,62 +132,55 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH const delta = chunk.choices[0]?.delta if ("reasoning" in delta && delta.reasoning) { - yield { - type: "reasoning", - text: delta.reasoning, - } as ApiStreamChunk + yield { type: "reasoning", text: delta.reasoning } as ApiStreamChunk } if (delta?.content) { fullResponseText += delta.content - yield { - type: "text", - text: delta.content, - } as ApiStreamChunk + yield { type: "text", text: delta.content } as ApiStreamChunk } - - // if (chunk.usage) { - // yield { - // type: "usage", - // inputTokens: chunk.usage.prompt_tokens || 0, - // outputTokens: chunk.usage.completion_tokens || 0, - // } - // } } - // Retry fetching generation details. + const endpoint = `${this.client.baseURL}/generation?id=${genId}` + + const config: AxiosRequestConfig = { + headers: { Authorization: `Bearer ${this.options.openRouterApiKey}` }, + timeout: 3_000, + } + let attempt = 0 + let lastError: Error | undefined + const startTime = Date.now() while (attempt++ < 10) { - await delay(200) // FIXME: necessary delay to ensure generation endpoint is ready + await delay(attempt * 100) // Give OpenRouter some time to produce the generation metadata. try { - const response = await axios.get(`${this.client.baseURL}/generation?id=${genId}`, { - headers: { - Authorization: `Bearer ${this.options.openRouterApiKey}`, - }, - timeout: 5_000, // this request hangs sometimes - }) - + const response = await axios.get(endpoint, config) const generation = response.data?.data yield { type: "usage", - // cacheWriteTokens: 0, - // cacheReadTokens: 0, - // openrouter generation endpoint fails often inputTokens: generation?.native_tokens_prompt || 0, outputTokens: generation?.native_tokens_completion || 0, totalCost: generation?.total_cost || 0, fullResponseText, } as OpenRouterApiStreamUsageChunk - return - } catch (error) { - // ignore if fails - console.error("Error fetching OpenRouter generation details:", error) + break + } catch (error: unknown) { + if (error instanceof Error) { + lastError = error + } } } + + if (lastError) { + console.error( + `Failed to fetch OpenRouter generation details after ${Date.now() - startTime}ms (${genId})`, + lastError, + ) + } } override getModel() { diff --git a/src/core/__tests__/contextProxy.test.ts b/src/core/__tests__/contextProxy.test.ts index e44f3e45b3..ea0cf8b578 100644 --- a/src/core/__tests__/contextProxy.test.ts +++ b/src/core/__tests__/contextProxy.test.ts @@ -1,15 +1,11 @@ +// npx jest src/core/__tests__/contextProxy.test.ts + import * as vscode from "vscode" import { ContextProxy } from "../contextProxy" + import { logger } from "../../utils/logging" -import { GLOBAL_STATE_KEYS, SECRET_KEYS } from "../../shared/globalState" +import { GLOBAL_STATE_KEYS, SECRET_KEYS, ConfigurationKey, GlobalStateKey } from "../../shared/globalState" -// Mock shared/globalState -jest.mock("../../shared/globalState", () => ({ - GLOBAL_STATE_KEYS: ["apiProvider", "apiModelId", "mode"], - SECRET_KEYS: ["apiKey", "openAiApiKey"], -})) - -// Mock VSCode API jest.mock("vscode", () => ({ Uri: { file: jest.fn((path) => ({ path })), @@ -27,7 +23,7 @@ describe("ContextProxy", () => { let mockGlobalState: any let mockSecrets: any - beforeEach(() => { + beforeEach(async () => { // Reset mocks jest.clearAllMocks() @@ -58,6 +54,7 @@ describe("ContextProxy", () => { // Create proxy instance proxy = new ContextProxy(mockContext) + await proxy.initialize() }) describe("read-only pass-through properties", () => { @@ -90,10 +87,10 @@ describe("ContextProxy", () => { describe("getGlobalState", () => { it("should return value from cache when it exists", async () => { // Manually set a value in the cache - await proxy.updateGlobalState("test-key", "cached-value") + await proxy.updateGlobalState("apiProvider", "cached-value") // Should return the cached value - const result = proxy.getGlobalState("test-key") + const result = proxy.getGlobalState("apiProvider") expect(result).toBe("cached-value") // Original context should be called once during updateGlobalState @@ -102,20 +99,20 @@ describe("ContextProxy", () => { it("should handle default values correctly", async () => { // No value in cache - const result = proxy.getGlobalState("unknown-key", "default-value") + const result = proxy.getGlobalState("apiProvider", "default-value") expect(result).toBe("default-value") }) }) describe("updateGlobalState", () => { it("should update state directly in original context", async () => { - await proxy.updateGlobalState("test-key", "new-value") + await proxy.updateGlobalState("apiProvider", "new-value") // Should have called original context - expect(mockGlobalState.update).toHaveBeenCalledWith("test-key", "new-value") + expect(mockGlobalState.update).toHaveBeenCalledWith("apiProvider", "new-value") // Should have stored the value in cache - const storedValue = await proxy.getGlobalState("test-key") + const storedValue = await proxy.getGlobalState("apiProvider") expect(storedValue).toBe("new-value") }) }) @@ -123,34 +120,34 @@ describe("ContextProxy", () => { describe("getSecret", () => { it("should return value from cache when it exists", async () => { // Manually set a value in the cache - await proxy.storeSecret("api-key", "cached-secret") + await proxy.storeSecret("apiKey", "cached-secret") // Should return the cached value - const result = proxy.getSecret("api-key") + const result = proxy.getSecret("apiKey") expect(result).toBe("cached-secret") }) }) describe("storeSecret", () => { it("should store secret directly in original context", async () => { - await proxy.storeSecret("api-key", "new-secret") + await proxy.storeSecret("apiKey", "new-secret") // Should have called original context - expect(mockSecrets.store).toHaveBeenCalledWith("api-key", "new-secret") + expect(mockSecrets.store).toHaveBeenCalledWith("apiKey", "new-secret") // Should have stored the value in cache - const storedValue = await proxy.getSecret("api-key") + const storedValue = await proxy.getSecret("apiKey") expect(storedValue).toBe("new-secret") }) it("should handle undefined value for secret deletion", async () => { - await proxy.storeSecret("api-key", undefined) + await proxy.storeSecret("apiKey", undefined) // Should have called delete on original context - expect(mockSecrets.delete).toHaveBeenCalledWith("api-key") + expect(mockSecrets.delete).toHaveBeenCalledWith("apiKey") // Should have stored undefined in cache - const storedValue = await proxy.getSecret("api-key") + const storedValue = await proxy.getSecret("apiKey") expect(storedValue).toBeUndefined() }) }) @@ -194,7 +191,7 @@ describe("ContextProxy", () => { const updateGlobalStateSpy = jest.spyOn(proxy, "updateGlobalState") // Test with an unknown key - await proxy.setValue("unknownKey", "some-value") + await proxy.setValue("unknownKey" as ConfigurationKey, "some-value") // Should have logged a warning expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("Unknown key: unknownKey")) @@ -203,7 +200,7 @@ describe("ContextProxy", () => { expect(updateGlobalStateSpy).toHaveBeenCalledWith("unknownKey", "some-value") // Should have stored the value in state cache - const storedValue = proxy.getGlobalState("unknownKey") + const storedValue = proxy.getGlobalState("unknownKey" as GlobalStateKey) expect(storedValue).toBe("some-value") }) }) @@ -241,18 +238,15 @@ describe("ContextProxy", () => { await proxy.setValues({ apiModelId: "gpt-4", // global state openAiApiKey: "test-api-key", // secret - unknownKey: "some-value", // unknown }) // Should have called appropriate methods expect(storeSecretSpy).toHaveBeenCalledWith("openAiApiKey", "test-api-key") expect(updateGlobalStateSpy).toHaveBeenCalledWith("apiModelId", "gpt-4") - expect(updateGlobalStateSpy).toHaveBeenCalledWith("unknownKey", "some-value") // Should have stored values in appropriate caches expect(proxy.getSecret("openAiApiKey")).toBe("test-api-key") expect(proxy.getGlobalState("apiModelId")).toBe("gpt-4") - expect(proxy.getGlobalState("unknownKey")).toBe("some-value") }) }) @@ -262,13 +256,11 @@ describe("ContextProxy", () => { await proxy.setValues({ apiModelId: "gpt-4", // global state openAiApiKey: "test-api-key", // secret - unknownKey: "some-value", // unknown }) // Verify initial state expect(proxy.getGlobalState("apiModelId")).toBe("gpt-4") expect(proxy.getSecret("openAiApiKey")).toBe("test-api-key") - expect(proxy.getGlobalState("unknownKey")).toBe("some-value") // Reset all state await proxy.resetAllState() @@ -277,7 +269,6 @@ describe("ContextProxy", () => { // Since our mock globalState.get returns undefined by default, // the cache should now contain undefined values expect(proxy.getGlobalState("apiModelId")).toBeUndefined() - expect(proxy.getGlobalState("unknownKey")).toBeUndefined() }) it("should update all global state keys to undefined", async () => { @@ -317,15 +308,13 @@ describe("ContextProxy", () => { it("should reinitialize caches after reset", async () => { // Spy on initialization methods - const initStateCache = jest.spyOn(proxy as any, "initializeStateCache") - const initSecretCache = jest.spyOn(proxy as any, "initializeSecretCache") + const initializeSpy = jest.spyOn(proxy as any, "initialize") // Reset all state await proxy.resetAllState() // Should reinitialize caches - expect(initStateCache).toHaveBeenCalledTimes(1) - expect(initSecretCache).toHaveBeenCalledTimes(1) + expect(initializeSpy).toHaveBeenCalledTimes(1) }) }) }) diff --git a/src/core/contextProxy.ts b/src/core/contextProxy.ts index 52197d99f3..b1cfc71a5a 100644 --- a/src/core/contextProxy.ts +++ b/src/core/contextProxy.ts @@ -1,97 +1,105 @@ import * as vscode from "vscode" + import { logger } from "../utils/logging" -import { GLOBAL_STATE_KEYS, SECRET_KEYS } from "../shared/globalState" +import { + GLOBAL_STATE_KEYS, + SECRET_KEYS, + GlobalStateKey, + SecretKey, + ConfigurationKey, + ConfigurationValues, + isSecretKey, + isGlobalStateKey, +} from "../shared/globalState" export class ContextProxy { private readonly originalContext: vscode.ExtensionContext - private stateCache: Map - private secretCache: Map + + private stateCache: Map + private secretCache: Map + private _isInitialized = false constructor(context: vscode.ExtensionContext) { - // Initialize properties first this.originalContext = context this.stateCache = new Map() this.secretCache = new Map() - - // Initialize state cache with all defined global state keys - this.initializeStateCache() - - // Initialize secret cache with all defined secret keys - this.initializeSecretCache() - - logger.debug("ContextProxy created") + this._isInitialized = false } - // Helper method to initialize state cache - private initializeStateCache(): void { + public get isInitialized() { + return this._isInitialized + } + + public async initialize() { for (const key of GLOBAL_STATE_KEYS) { try { - const value = this.originalContext.globalState.get(key) - this.stateCache.set(key, value) + this.stateCache.set(key, this.originalContext.globalState.get(key)) } catch (error) { logger.error(`Error loading global ${key}: ${error instanceof Error ? error.message : String(error)}`) } } + + const promises = SECRET_KEYS.map(async (key) => { + try { + this.secretCache.set(key, await this.originalContext.secrets.get(key)) + } catch (error) { + logger.error(`Error loading secret ${key}: ${error instanceof Error ? error.message : String(error)}`) + } + }) + + await Promise.all(promises) + + this._isInitialized = true } - // Helper method to initialize secret cache - private initializeSecretCache(): void { - for (const key of SECRET_KEYS) { - // Get actual value and update cache when promise resolves - ;(this.originalContext.secrets.get(key) as Promise) - .then((value) => { - this.secretCache.set(key, value) - }) - .catch((error: Error) => { - logger.error(`Error loading secret ${key}: ${error.message}`) - }) - } - } - - get extensionUri(): vscode.Uri { + get extensionUri() { return this.originalContext.extensionUri } - get extensionPath(): string { + + get extensionPath() { return this.originalContext.extensionPath } - get globalStorageUri(): vscode.Uri { + + get globalStorageUri() { return this.originalContext.globalStorageUri } - get logUri(): vscode.Uri { + + get logUri() { return this.originalContext.logUri } - get extension(): vscode.Extension | undefined { + + get extension() { return this.originalContext.extension } - get extensionMode(): vscode.ExtensionMode { + + get extensionMode() { return this.originalContext.extensionMode } - getGlobalState(key: string): T | undefined - getGlobalState(key: string, defaultValue: T): T - getGlobalState(key: string, defaultValue?: T): T | undefined { + getGlobalState(key: GlobalStateKey): T | undefined + getGlobalState(key: GlobalStateKey, defaultValue: T): T + getGlobalState(key: GlobalStateKey, defaultValue?: T): T | undefined { const value = this.stateCache.get(key) as T | undefined return value !== undefined ? value : (defaultValue as T | undefined) } - updateGlobalState(key: string, value: T): Thenable { + updateGlobalState(key: GlobalStateKey, value: T) { this.stateCache.set(key, value) return this.originalContext.globalState.update(key, value) } - getSecret(key: string): string | undefined { + getSecret(key: SecretKey) { return this.secretCache.get(key) } - storeSecret(key: string, value?: string): Thenable { - // Update cache + storeSecret(key: SecretKey, value?: string) { + // Update cache. this.secretCache.set(key, value) - // Write directly to context - if (value === undefined) { - return this.originalContext.secrets.delete(key) - } else { - return this.originalContext.secrets.store(key, value) - } + + // Write directly to context. + return value === undefined + ? this.originalContext.secrets.delete(key) + : this.originalContext.secrets.store(key, value) } /** * Set a value in either secrets or global state based on key type. @@ -101,17 +109,15 @@ export class ContextProxy { * @param value The value to set * @returns A promise that resolves when the operation completes */ - setValue(key: string, value: any): Thenable { - if (SECRET_KEYS.includes(key as any)) { + setValue(key: ConfigurationKey, value: any) { + if (isSecretKey(key)) { return this.storeSecret(key, value) - } - - if (GLOBAL_STATE_KEYS.includes(key as any)) { + } else if (isGlobalStateKey(key)) { + return this.updateGlobalState(key, value) + } else { + logger.warn(`Unknown key: ${key}. Storing as global state.`) return this.updateGlobalState(key, value) } - - logger.warn(`Unknown key: ${key}. Storing as global state.`) - return this.updateGlobalState(key, value) } /** @@ -120,14 +126,14 @@ export class ContextProxy { * @param values An object containing key-value pairs to set * @returns A promise that resolves when all operations complete */ - async setValues(values: Record): Promise { + async setValues(values: Partial) { const promises: Thenable[] = [] for (const [key, value] of Object.entries(values)) { - promises.push(this.setValue(key, value)) + promises.push(this.setValue(key as ConfigurationKey, value)) } - return Promise.all(promises) + await Promise.all(promises) } /** @@ -135,23 +141,22 @@ export class ContextProxy { * This clears all data from both the in-memory caches and the VSCode storage. * @returns A promise that resolves when all reset operations are complete */ - async resetAllState(): Promise { + async resetAllState() { // Clear in-memory caches this.stateCache.clear() this.secretCache.clear() - // Reset all global state values to undefined + // Reset all global state values to undefined. const stateResetPromises = GLOBAL_STATE_KEYS.map((key) => this.originalContext.globalState.update(key, undefined), ) - // Delete all secrets + // Delete all secrets. const secretResetPromises = SECRET_KEYS.map((key) => this.originalContext.secrets.delete(key)) - // Wait for all reset operations to complete + // Wait for all reset operations to complete. await Promise.all([...stateResetPromises, ...secretResetPromises]) - this.initializeStateCache() - this.initializeSecretCache() + this.initialize() } } diff --git a/src/core/prompts/sections/modes.ts b/src/core/prompts/sections/modes.ts index 788c7d7ebe..78b94ec9e7 100644 --- a/src/core/prompts/sections/modes.ts +++ b/src/core/prompts/sections/modes.ts @@ -12,7 +12,7 @@ export async function getModesSection(context: vscode.ExtensionContext): Promise const allModes = await getAllModesWithPrompts(context) // Get enableCustomModeCreation setting from extension state - const shouldEnableCustomModeCreation = await context.globalState.get("enableCustomModeCreation") ?? true + const shouldEnableCustomModeCreation = (await context.globalState.get("enableCustomModeCreation")) ?? true let modesContent = `==== diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index b41b973f11..7a61e1698d 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -6,7 +6,6 @@ import os from "os" import pWaitFor from "p-wait-for" import * as path from "path" import * as vscode from "vscode" -import simpleGit from "simple-git" import { setPanel } from "../../activate/registerCommands" import { ApiConfiguration, ApiProvider, ModelInfo, API_CONFIG_KEYS } from "../../shared/api" @@ -14,7 +13,13 @@ import { CheckpointStorage } from "../../shared/checkpoints" import { findLast } from "../../shared/array" import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt" import { GlobalFileNames } from "../../shared/globalFileNames" -import { SecretKey, GlobalStateKey, SECRET_KEYS, GLOBAL_STATE_KEYS } from "../../shared/globalState" +import { + SecretKey, + GlobalStateKey, + SECRET_KEYS, + GLOBAL_STATE_KEYS, + ConfigurationValues, +} from "../../shared/globalState" import { HistoryItem } from "../../shared/HistoryItem" import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage" import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage" @@ -387,6 +392,11 @@ export class ClineProvider implements vscode.WebviewViewProvider { async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) { this.outputChannel.appendLine("Resolving webview view") + + if (!this.contextProxy.isInitialized) { + await this.contextProxy.initialize() + } + this.view = webviewView // Set panel reference according to webview type @@ -2013,18 +2023,19 @@ export class ClineProvider implements vscode.WebviewViewProvider { } private async updateApiConfiguration(apiConfiguration: ApiConfiguration) { - // Update mode's default config + // Update mode's default config. const { mode } = await this.getState() + if (mode) { const currentApiConfigName = await this.getGlobalState("currentApiConfigName") const listApiConfig = await this.configManager.listConfig() const config = listApiConfig?.find((c) => c.name === currentApiConfigName) + if (config?.id) { await this.configManager.setModeConfig(mode, config.id) } } - // Use the new setValues method to handle routing values to secrets or global state await this.contextProxy.setValues(apiConfiguration) if (this.getCurrentCline()) { @@ -2620,11 +2631,11 @@ export class ClineProvider implements vscode.WebviewViewProvider { // global - async updateGlobalState(key: GlobalStateKey, value: any) { + public async updateGlobalState(key: GlobalStateKey, value: any) { await this.contextProxy.updateGlobalState(key, value) } - async getGlobalState(key: GlobalStateKey) { + public async getGlobalState(key: GlobalStateKey) { return await this.contextProxy.getGlobalState(key) } @@ -2638,6 +2649,12 @@ export class ClineProvider implements vscode.WebviewViewProvider { return await this.contextProxy.getSecret(key) } + // global + secret + + public async setValues(values: Partial) { + await this.contextProxy.setValues(values) + } + // dev async resetState() { diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index be77b13c7a..177373ef1e 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -19,6 +19,8 @@ jest.mock("../../contextProxy", () => { return { ContextProxy: jest.fn().mockImplementation((context) => ({ originalContext: context, + isInitialized: true, + initialize: jest.fn(), extensionUri: context.extensionUri, extensionPath: context.extensionPath, globalStorageUri: context.globalStorageUri, diff --git a/src/exports/README.md b/src/exports/README.md index 0554580836..36b7c23d55 100644 --- a/src/exports/README.md +++ b/src/exports/README.md @@ -19,13 +19,6 @@ if (!api) { throw new Error("API is not available") } -// Set custom instructions. -await api.setCustomInstructions("Talk like a pirate") - -// Get custom instructions. -const instructions = await api.getCustomInstructions() -console.log("Current custom instructions:", instructions) - // Start a new task with an initial message. await api.startNewTask("Hello, Roo Code API! Let's make a new project...") diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index e310fef6a7..04f6bdd742 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -1,16 +1,4 @@ export interface RooCodeAPI { - /** - * Sets the custom instructions in the global storage. - * @param value The custom instructions to be saved. - */ - setCustomInstructions(value: string): Promise - - /** - * Retrieves the custom instructions from the global storage. - * @returns The saved custom instructions, or undefined if not set. - */ - getCustomInstructions(): Promise - /** * Starts a new task with an optional initial message and images. * @param task Optional initial task message. @@ -18,6 +6,11 @@ export interface RooCodeAPI { */ startNewTask(task?: string, images?: string[]): Promise + /** + * Cancels the current task. + */ + cancelTask(): Promise + /** * Sends a message to the current task. * @param message Optional message to send. @@ -36,9 +29,20 @@ export interface RooCodeAPI { pressSecondaryButton(): Promise /** - * The sidebar provider instance. + * Sets the configuration for the current task. + * @param values An object containing key-value pairs to set. */ - sidebarProvider: ClineProvider + setConfiguration(values: Partial): Promise + + /** + * Returns true if the API is ready to use. + */ + isReady(): boolean + + /** + * Returns the messages from the current task. + */ + getMessages(): ClineMessage[] } export type ClineAsk = @@ -95,84 +99,106 @@ export interface ClineMessage { progressStatus?: ToolProgressStatus } -export interface ClineProvider { - readonly context: vscode.ExtensionContext - readonly viewLaunched: boolean - readonly messages: ClineMessage[] +export type SecretKey = + | "apiKey" + | "glamaApiKey" + | "openRouterApiKey" + | "awsAccessKey" + | "awsSecretKey" + | "awsSessionToken" + | "openAiApiKey" + | "geminiApiKey" + | "openAiNativeApiKey" + | "deepSeekApiKey" + | "mistralApiKey" + | "unboundApiKey" + | "requestyApiKey" - /** - * Resolves the webview view for the provider - * @param webviewView The webview view or panel to resolve - */ - resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel): Promise +export type GlobalStateKey = + | "apiProvider" + | "apiModelId" + | "glamaModelId" + | "glamaModelInfo" + | "awsRegion" + | "awsUseCrossRegionInference" + | "awsProfile" + | "awsUseProfile" + | "awsCustomArn" + | "vertexKeyFile" + | "vertexJsonCredentials" + | "vertexProjectId" + | "vertexRegion" + | "lastShownAnnouncementId" + | "customInstructions" + | "alwaysAllowReadOnly" + | "alwaysAllowWrite" + | "alwaysAllowExecute" + | "alwaysAllowBrowser" + | "alwaysAllowMcp" + | "alwaysAllowModeSwitch" + | "alwaysAllowSubtasks" + | "taskHistory" + | "openAiBaseUrl" + | "openAiModelId" + | "openAiCustomModelInfo" + | "openAiUseAzure" + | "ollamaModelId" + | "ollamaBaseUrl" + | "lmStudioModelId" + | "lmStudioBaseUrl" + | "anthropicBaseUrl" + | "modelMaxThinkingTokens" + | "azureApiVersion" + | "openAiStreamingEnabled" + | "openRouterModelId" + | "openRouterModelInfo" + | "openRouterBaseUrl" + | "openRouterUseMiddleOutTransform" + | "allowedCommands" + | "soundEnabled" + | "soundVolume" + | "diffEnabled" + | "enableCheckpoints" + | "checkpointStorage" + | "browserViewportSize" + | "screenshotQuality" + | "remoteBrowserHost" + | "fuzzyMatchThreshold" + | "preferredLanguage" // Language setting for Cline's communication + | "writeDelayMs" + | "terminalOutputLineLimit" + | "mcpEnabled" + | "enableMcpServerCreation" + | "alwaysApproveResubmit" + | "requestDelaySeconds" + | "rateLimitSeconds" + | "currentApiConfigName" + | "listApiConfigMeta" + | "vsCodeLmModelSelector" + | "mode" + | "modeApiConfigs" + | "customModePrompts" + | "customSupportPrompts" + | "enhancementApiConfigId" + | "experiments" // Map of experiment IDs to their enabled state + | "autoApprovalEnabled" + | "enableCustomModeCreation" // Enable the ability for Roo to create custom modes + | "customModes" // Array of custom modes + | "unboundModelId" + | "requestyModelId" + | "requestyModelInfo" + | "unboundModelInfo" + | "modelTemperature" + | "modelMaxTokens" + | "mistralCodestralUrl" + | "maxOpenTabsContext" + | "browserToolEnabled" + | "lmStudioSpeculativeDecodingEnabled" + | "lmStudioDraftModelId" + | "telemetrySetting" + | "showRooIgnoredFiles" + | "remoteBrowserEnabled" - /** - * Initializes Cline with a task - */ - initClineWithTask(task?: string, images?: string[]): Promise +export type ConfigurationKey = GlobalStateKey | SecretKey - /** - * Initializes Cline with a history item - */ - initClineWithHistoryItem(historyItem: HistoryItem): Promise - - /** - * Posts a message to the webview - */ - postMessageToWebview(message: ExtensionMessage): Promise - - /** - * Handles mode switching - */ - handleModeSwitch(newMode: Mode): Promise - - /** - * Updates custom instructions - */ - updateCustomInstructions(instructions?: string): Promise - - /** - * Cancels the current task - */ - cancelTask(): Promise - - /** - * Gets the current state - */ - getState(): Promise - - /** - * Updates a value in the global state - * @param key The key to update - * @param value The value to set - */ - updateGlobalState(key: GlobalStateKey, value: any): Promise - - /** - * Gets a value from the global state - * @param key The key to get - */ - getGlobalState(key: GlobalStateKey): Promise - - /** - * Stores a secret value in secure storage - * @param key The key to store the secret under - * @param value The secret value to store, or undefined to remove the secret - */ - storeSecret(key: SecretKey, value?: string): Promise - - /** - * Resets the state - */ - resetState(): Promise - - /** - * Logs a message - */ - log(message: string): void - - /** - * Disposes of the provider - */ - dispose(): Promise -} +export type ConfigurationValues = Record diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index ba63c16eb6..3622562506 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -7,7 +7,7 @@ import { CustomSupportPrompts } from "./support-prompt" import { ExperimentId } from "./experiments" import { CheckpointStorage } from "./checkpoints" import { TelemetrySetting } from "./TelemetrySetting" -import { ClineMessage, ClineAsk, ClineSay } from "../exports/roo-code" +import type { ClineMessage, ClineAsk, ClineSay } from "../exports/roo-code" export interface LanguageModelChatSelector { vendor?: string diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index d03f5f20a4..d3ddb10326 100644 --- a/src/shared/globalState.ts +++ b/src/shared/globalState.ts @@ -1,4 +1,17 @@ -// Define the array first with 'as const' to create a readonly tuple type +import type { SecretKey, GlobalStateKey, ConfigurationKey, ConfigurationValues } from "../exports/roo-code" + +export type { SecretKey, GlobalStateKey, ConfigurationKey, ConfigurationValues } + +/** + * For convenience we'd like the `RooCodeAPI` to define `SecretKey` and `GlobalStateKey`, + * but since it is a type definition file we can't export constants without some + * annoyances. In order to acheive proper type safety without using constants as + * in the type definition we use this clever Check<>Exhaustiveness pattern. + * If you extend the `SecretKey` or `GlobalStateKey` types, you will need to + * update the `SECRET_KEYS` and `GLOBAL_STATE_KEYS` arrays to include the new + * keys or a type error will be thrown. + */ + export const SECRET_KEYS = [ "apiKey", "glamaApiKey", @@ -15,10 +28,10 @@ export const SECRET_KEYS = [ "requestyApiKey", ] as const -// Derive the type from the array - creates a union of string literals -export type SecretKey = (typeof SECRET_KEYS)[number] +type CheckSecretKeysExhaustiveness = Exclude extends never ? true : false + +const _checkSecretKeysExhaustiveness: CheckSecretKeysExhaustiveness = true -// Define the array first with 'as const' to create a readonly tuple type export const GLOBAL_STATE_KEYS = [ "apiProvider", "apiModelId", @@ -69,7 +82,7 @@ export const GLOBAL_STATE_KEYS = [ "screenshotQuality", "remoteBrowserHost", "fuzzyMatchThreshold", - "preferredLanguage", // Language setting for Cline's communication + "preferredLanguage", // Language setting for Cline's communication. "writeDelayMs", "terminalOutputLineLimit", "mcpEnabled", @@ -85,10 +98,10 @@ export const GLOBAL_STATE_KEYS = [ "customModePrompts", "customSupportPrompts", "enhancementApiConfigId", - "experiments", // Map of experiment IDs to their enabled state + "experiments", // Map of experiment IDs to their enabled state. "autoApprovalEnabled", - "enableCustomModeCreation", // Enable the ability for Roo to create custom modes - "customModes", // Array of custom modes + "enableCustomModeCreation", // Enable the ability for Roo to create custom modes. + "customModes", // Array of custom modes. "unboundModelId", "requestyModelId", "requestyModelInfo", @@ -105,5 +118,12 @@ export const GLOBAL_STATE_KEYS = [ "remoteBrowserEnabled", ] as const -// Derive the type from the array - creates a union of string literals -export type GlobalStateKey = (typeof GLOBAL_STATE_KEYS)[number] +type CheckGlobalStateKeysExhaustiveness = + Exclude extends never ? true : false + +const _checkGlobalStateKeysExhaustiveness: CheckGlobalStateKeysExhaustiveness = true + +export const isSecretKey = (key: string): key is SecretKey => SECRET_KEYS.includes(key as SecretKey) + +export const isGlobalStateKey = (key: string): key is GlobalStateKey => + GLOBAL_STATE_KEYS.includes(key as GlobalStateKey) From 80d1fae033dabdbbdeeb0ba5cb6e6dc6f32c87e6 Mon Sep 17 00:00:00 2001 From: Chris Estreich Date: Tue, 11 Mar 2025 14:27:18 -0700 Subject: [PATCH 3/9] Update src/shared/globalState.ts Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> --- src/shared/globalState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index d3ddb10326..a3dbb2b710 100644 --- a/src/shared/globalState.ts +++ b/src/shared/globalState.ts @@ -5,7 +5,7 @@ export type { SecretKey, GlobalStateKey, ConfigurationKey, ConfigurationValues } /** * For convenience we'd like the `RooCodeAPI` to define `SecretKey` and `GlobalStateKey`, * but since it is a type definition file we can't export constants without some - * annoyances. In order to acheive proper type safety without using constants as + * annoyances. In order to achieve proper type safety without using constants as * in the type definition we use this clever Check<>Exhaustiveness pattern. * If you extend the `SecretKey` or `GlobalStateKey` types, you will need to * update the `SECRET_KEYS` and `GLOBAL_STATE_KEYS` arrays to include the new From 421e1972ad901acbd987229bcfa0e1ffae10d553 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 11 Mar 2025 00:43:18 -0400 Subject: [PATCH 4/9] Add prompt caching to OpenAI-compatible custom model info --- .changeset/thin-fans-deliver.md | 5 + .../src/components/settings/ApiOptions.tsx | 114 +++++++++++++++++- 2 files changed, 117 insertions(+), 2 deletions(-) create mode 100644 .changeset/thin-fans-deliver.md diff --git a/.changeset/thin-fans-deliver.md b/.changeset/thin-fans-deliver.md new file mode 100644 index 0000000000..5244b7d0d7 --- /dev/null +++ b/.changeset/thin-fans-deliver.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Add prompt caching to OpenAI-compatible custom model info diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index 656a6831cb..049292767f 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -819,7 +819,7 @@ const ApiOptions = ({ style={{ fontSize: "12px" }} /> -
+
Is this model capable of processing and understanding images?
@@ -842,11 +842,34 @@ const ApiOptions = ({ style={{ fontSize: "12px" }} /> -
+
Is this model capable of interacting with a browser? (e.g. Claude 3.7 Sonnet).
+
+
+ { + return { + ...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults), + supportsPromptCache: checked, + } + })}> + Prompt Caching + + +
+
+ Is this model capable of caching prompts? +
+
+
+ {apiConfiguration?.openAiCustomModelInfo?.supportsPromptCache && ( + <> +
+ { + const value = apiConfiguration?.openAiCustomModelInfo?.cacheReadsPrice + + if (!value && value !== 0) { + return "var(--vscode-input-border)" + } + + return value >= 0 + ? "var(--vscode-charts-green)" + : "var(--vscode-errorForeground)" + })(), + }} + onChange={handleInputChange("openAiCustomModelInfo", (e) => { + const value = (e.target as HTMLInputElement).value + const parsed = parseFloat(value) + + return { + ...(apiConfiguration?.openAiCustomModelInfo ?? + openAiModelInfoSaneDefaults), + cacheReadsPrice: isNaN(parsed) ? 0 : parsed, + } + })} + placeholder="e.g. 0.0001" + className="w-full"> +
+ Cache Reads Price + +
+
+
+
+ { + const value = apiConfiguration?.openAiCustomModelInfo?.cacheWritesPrice + + if (!value && value !== 0) { + return "var(--vscode-input-border)" + } + + return value >= 0 + ? "var(--vscode-charts-green)" + : "var(--vscode-errorForeground)" + })(), + }} + onChange={handleInputChange("openAiCustomModelInfo", (e) => { + const value = (e.target as HTMLInputElement).value + const parsed = parseFloat(value) + + return { + ...(apiConfiguration?.openAiCustomModelInfo ?? + openAiModelInfoSaneDefaults), + cacheWritesPrice: isNaN(parsed) ? 0 : parsed, + } + })} + placeholder="e.g. 0.00005" + className="w-full"> +
+ Cache Writes Price + +
+
+
+ + )} + - - - - - - - {[ - { value: "English", label: "English" }, - { value: "Arabic", label: "Arabic - العربية" }, - { - value: "Brazilian Portuguese", - label: "Portuguese - Português (Brasil)", - }, - { value: "Catalan", label: "Catalan - Català" }, - { value: "Czech", label: "Czech - Čeština" }, - { value: "French", label: "French - Français" }, - { value: "German", label: "German - Deutsch" }, - { value: "Hindi", label: "Hindi - हिन्दी" }, - { value: "Hungarian", label: "Hungarian - Magyar" }, - { value: "Italian", label: "Italian - Italiano" }, - { value: "Japanese", label: "Japanese - 日本語" }, - { value: "Korean", label: "Korean - 한국어" }, - { value: "Polish", label: "Polish - Polski" }, - { value: "Portuguese", label: "Portuguese - Português (Portugal)" }, - { value: "Russian", label: "Russian - Русский" }, - { value: "Simplified Chinese", label: "Simplified Chinese - 简体中文" }, - { value: "Spanish", label: "Spanish - Español" }, - { - value: "Traditional Chinese", - label: "Traditional Chinese - 繁體中文", - }, - { value: "Turkish", label: "Turkish - Türkçe" }, - ].map((language) => ( - { - setPreferredLanguage(value) - vscode.postMessage({ - type: "preferredLanguage", - text: value, - }) - setOpen(false) - }}> - {language.label} - - - ))} - - - -
- -
-
- -

- Select the language that Roo should use for communication. -

- -
Custom Instructions for All Modes
These instructions apply to all modes. They provide a base set of behaviors that can be enhanced @@ -1509,41 +1404,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
)} - - {isCustomLanguage && ( -
-
-

Add Custom Language

- setCustomLanguage(e.target.value)} - /> -
- - -
-
-
- )} ) } diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index fc5e5b69af..f7292e2721 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -39,8 +39,6 @@ export interface ExtensionStateContextType extends ExtensionState { setEnableCheckpoints: (value: boolean) => void setBrowserViewportSize: (value: string) => void setFuzzyMatchThreshold: (value: number) => void - preferredLanguage: string - setPreferredLanguage: (value: string) => void setWriteDelayMs: (value: number) => void screenshotQuality?: number setScreenshotQuality: (value: number) => void @@ -118,7 +116,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode enableCheckpoints: true, checkpointStorage: "task", fuzzyMatchThreshold: 1.0, - preferredLanguage: "English", + vscodeLanguage: "en", // Default language code enableCustomModeCreation: true, writeDelayMs: 1000, browserViewportSize: "900x600", @@ -260,7 +258,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setBrowserViewportSize: (value: string) => setState((prevState) => ({ ...prevState, browserViewportSize: value })), setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })), - setPreferredLanguage: (value) => setState((prevState) => ({ ...prevState, preferredLanguage: value })), setWriteDelayMs: (value) => setState((prevState) => ({ ...prevState, writeDelayMs: value })), setScreenshotQuality: (value) => setState((prevState) => ({ ...prevState, screenshotQuality: value })), setTerminalOutputLineLimit: (value) => diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx index 0f66413b50..f8acf4f4d3 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx @@ -109,7 +109,6 @@ describe("mergeExtensionState", () => { shouldShowAnnouncement: false, enableCheckpoints: true, checkpointStorage: "task", - preferredLanguage: "English", writeDelayMs: 1000, requestDelaySeconds: 5, rateLimitSeconds: 0, From 00bf91470bd90f444cdfac8b00efdd41fbccdf26 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 11 Mar 2025 11:12:48 -0400 Subject: [PATCH 8/9] Add i18n --- .roomodes | 29 ++ src/core/Cline.ts | 3 +- src/core/prompts/__tests__/sections.test.ts | 2 +- src/core/prompts/__tests__/system.test.ts | 10 +- .../__tests__/custom-instructions.test.ts | 2 +- .../prompts/sections/custom-instructions.ts | 8 +- src/core/prompts/system.ts | 5 +- src/core/webview/ClineProvider.ts | 5 +- .../webview/__tests__/ClineProvider.test.ts | 4 +- src/shared/ExtensionMessage.ts | 2 +- src/shared/__tests__/language.test.ts | 19 + src/shared/__tests__/modes.test.ts | 4 +- src/shared/language.ts | 14 + src/shared/modes.ts | 4 +- webview-ui/jest.config.cjs | 8 +- webview-ui/package-lock.json | 489 +++++------------- webview-ui/package.json | 6 +- webview-ui/src/App.tsx | 6 +- .../src/__mocks__/i18n/TranslationContext.tsx | 47 ++ webview-ui/src/__mocks__/i18n/setup.ts | 62 +++ webview-ui/src/components/chat/ChatView.tsx | 5 +- .../src/components/ui/combobox-primitive.tsx | 2 +- .../src/context/ExtensionStateContext.tsx | 2 +- webview-ui/src/i18n/TranslationContext.tsx | 51 ++ .../__tests__/TranslationContext.test.tsx | 52 ++ webview-ui/src/i18n/locales/ar/chat.json | 3 + webview-ui/src/i18n/locales/ca/chat.json | 3 + webview-ui/src/i18n/locales/cs/chat.json | 3 + webview-ui/src/i18n/locales/de/chat.json | 3 + webview-ui/src/i18n/locales/en/chat.json | 3 + webview-ui/src/i18n/locales/es/chat.json | 3 + webview-ui/src/i18n/locales/fr/chat.json | 3 + webview-ui/src/i18n/locales/hi/chat.json | 3 + webview-ui/src/i18n/locales/hu/chat.json | 3 + webview-ui/src/i18n/locales/it/chat.json | 3 + webview-ui/src/i18n/locales/ja/chat.json | 3 + webview-ui/src/i18n/locales/ko/chat.json | 3 + webview-ui/src/i18n/locales/pl/chat.json | 3 + webview-ui/src/i18n/locales/pt-br/chat.json | 3 + webview-ui/src/i18n/locales/pt/chat.json | 3 + webview-ui/src/i18n/locales/ru/chat.json | 3 + webview-ui/src/i18n/locales/tr/chat.json | 3 + webview-ui/src/i18n/locales/zh-cn/chat.json | 3 + webview-ui/src/i18n/locales/zh-tw/chat.json | 3 + webview-ui/src/i18n/setup.ts | 54 ++ webview-ui/src/i18n/test-utils.ts | 37 ++ 46 files changed, 603 insertions(+), 386 deletions(-) create mode 100644 .roomodes create mode 100644 src/shared/__tests__/language.test.ts create mode 100644 src/shared/language.ts create mode 100644 webview-ui/src/__mocks__/i18n/TranslationContext.tsx create mode 100644 webview-ui/src/__mocks__/i18n/setup.ts create mode 100644 webview-ui/src/i18n/TranslationContext.tsx create mode 100644 webview-ui/src/i18n/__tests__/TranslationContext.test.tsx create mode 100644 webview-ui/src/i18n/locales/ar/chat.json create mode 100644 webview-ui/src/i18n/locales/ca/chat.json create mode 100644 webview-ui/src/i18n/locales/cs/chat.json create mode 100644 webview-ui/src/i18n/locales/de/chat.json create mode 100644 webview-ui/src/i18n/locales/en/chat.json create mode 100644 webview-ui/src/i18n/locales/es/chat.json create mode 100644 webview-ui/src/i18n/locales/fr/chat.json create mode 100644 webview-ui/src/i18n/locales/hi/chat.json create mode 100644 webview-ui/src/i18n/locales/hu/chat.json create mode 100644 webview-ui/src/i18n/locales/it/chat.json create mode 100644 webview-ui/src/i18n/locales/ja/chat.json create mode 100644 webview-ui/src/i18n/locales/ko/chat.json create mode 100644 webview-ui/src/i18n/locales/pl/chat.json create mode 100644 webview-ui/src/i18n/locales/pt-br/chat.json create mode 100644 webview-ui/src/i18n/locales/pt/chat.json create mode 100644 webview-ui/src/i18n/locales/ru/chat.json create mode 100644 webview-ui/src/i18n/locales/tr/chat.json create mode 100644 webview-ui/src/i18n/locales/zh-cn/chat.json create mode 100644 webview-ui/src/i18n/locales/zh-tw/chat.json create mode 100644 webview-ui/src/i18n/setup.ts create mode 100644 webview-ui/src/i18n/test-utils.ts diff --git a/.roomodes b/.roomodes new file mode 100644 index 0000000000..0ccbb47432 --- /dev/null +++ b/.roomodes @@ -0,0 +1,29 @@ +{ + "customModes": [ + { + "slug": "translator", + "name": "Translator", + "roleDefinition": "You are Roo, a linguistic specialist focused on translating and managing localization files. Your responsibility is to help maintain and update translation files for the application, ensuring consistency and accuracy across all language resources.", + "groups": [ + "read", + ["edit", { "fileRegex": "src/i18n/locales/", "description": "Translation files only" }] + ], + "customInstructions": "When translating content:\n- Maintain consistent terminology across all translations\n- Respect the JSON structure of translation files\n- Consider context when translating UI strings\n- Watch for placeholders (like {{variable}}) and preserve them in translations\n- Be mindful of text length in UI elements when translating to languages that might require more characters\n- If you need context for a translation, use read_file to examine the components using these strings" + }, + { + "slug": "test", + "name": "Test", + "roleDefinition": "You are Roo, a Jest testing specialist with deep expertise in:\n- Writing and maintaining Jest test suites\n- Test-driven development (TDD) practices\n- Mocking and stubbing with Jest\n- Integration testing strategies\n- TypeScript testing patterns\n- Code coverage analysis\n- Test performance optimization\n\nYour focus is on maintaining high test quality and coverage across the codebase, working primarily with:\n- Test files in __tests__ directories\n- Mock implementations in __mocks__\n- Test utilities and helpers\n- Jest configuration and setup\n\nYou ensure tests are:\n- Well-structured and maintainable\n- Following Jest best practices\n- Properly typed with TypeScript\n- Providing meaningful coverage\n- Using appropriate mocking strategies", + "groups": [ + "read", + "browser", + "command", + ["edit", { + "fileRegex": "(__tests__/.*|__mocks__/.*|\\.test\\.(ts|tsx|js|jsx)$|/test/.*|jest\\.config\\.(js|ts)$)", + "description": "Test files, mocks, and Jest configuration" + }] + ], + "customInstructions": "When writing tests:\n- Always use describe/it blocks for clear test organization\n- Include meaningful test descriptions\n- Use beforeEach/afterEach for proper test isolation\n- Implement proper error cases\n- Add JSDoc comments for complex test scenarios\n- Ensure mocks are properly typed\n- Verify both positive and negative test cases" + } + ] +} \ No newline at end of file diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 4f406dcd97..28bdab1ed0 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -70,6 +70,7 @@ import { truncateConversationIfNeeded } from "./sliding-window" import { ClineProvider } from "./webview/ClineProvider" import { detectCodeOmission } from "../integrations/editor/detect-omission" import { BrowserSession } from "../services/browser/BrowserSession" +import { formatLanguage } from "../shared/language" import { McpHub } from "../services/mcp/McpHub" import crypto from "crypto" import { insertGroups } from "./diff/insert-groups" @@ -3668,7 +3669,7 @@ export class Cline { const modeDetails = await getFullModeDetails(currentMode, customModes, customModePrompts, { cwd, globalCustomInstructions, - vscodeLanguage: vscode.env.language, + language: formatLanguage(vscode.env.language), }) details += `\n\n# Current Mode\n` details += `${currentMode}\n` diff --git a/src/core/prompts/__tests__/sections.test.ts b/src/core/prompts/__tests__/sections.test.ts index 4c43a1ba56..630da9064b 100644 --- a/src/core/prompts/__tests__/sections.test.ts +++ b/src/core/prompts/__tests__/sections.test.ts @@ -9,7 +9,7 @@ describe("addCustomInstructions", () => { "global instructions", "/test/path", "test-mode", - { vscodeLanguage: "fr" }, + { language: "fr" }, ) expect(result).toContain("Language Preference:") diff --git a/src/core/prompts/__tests__/system.test.ts b/src/core/prompts/__tests__/system.test.ts index 38f297dcd0..9750d13b3c 100644 --- a/src/core/prompts/__tests__/system.test.ts +++ b/src/core/prompts/__tests__/system.test.ts @@ -37,14 +37,14 @@ __setMockImplementation( globalCustomInstructions: string, cwd: string, mode: string, - options?: { vscodeLanguage?: string }, + options?: { language?: string }, ) => { const sections = [] // Add language preference if provided - if (options?.vscodeLanguage) { + if (options?.language) { sections.push( - `Language Preference:\nYou should always speak and think in the "${options.vscodeLanguage}" language.`, + `Language Preference:\nYou should always speak and think in the "${options.language}" language.`, ) } @@ -792,7 +792,7 @@ describe("addCustomInstructions", () => { it("should include preferred language when provided", async () => { const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug, { - vscodeLanguage: "es", + language: "es", }) expect(instructions).toMatchSnapshot() }) @@ -808,7 +808,7 @@ describe("addCustomInstructions", () => { "", "/test/path", defaultModeSlug, - { vscodeLanguage: "fr" }, + { language: "fr" }, ) expect(instructions).toMatchSnapshot() }) diff --git a/src/core/prompts/sections/__tests__/custom-instructions.test.ts b/src/core/prompts/sections/__tests__/custom-instructions.test.ts index 0d30aaea9a..1922a22efc 100644 --- a/src/core/prompts/sections/__tests__/custom-instructions.test.ts +++ b/src/core/prompts/sections/__tests__/custom-instructions.test.ts @@ -113,7 +113,7 @@ describe("addCustomInstructions", () => { "global instructions", "/fake/path", "test-mode", - { vscodeLanguage: "es" }, + { language: "es" }, ) expect(result).toContain("Language Preference:") diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts index 251f2ad6cb..b50219d563 100644 --- a/src/core/prompts/sections/custom-instructions.ts +++ b/src/core/prompts/sections/custom-instructions.ts @@ -34,7 +34,7 @@ export async function addCustomInstructions( globalCustomInstructions: string, cwd: string, mode: string, - options: { vscodeLanguage?: string; rooIgnoreInstructions?: string } = {}, + options: { language?: string; rooIgnoreInstructions?: string } = {}, ): Promise { const sections = [] @@ -46,10 +46,8 @@ export async function addCustomInstructions( } // Add language preference if provided - if (options.vscodeLanguage) { - sections.push( - `Language Preference:\nYou should always speak and think in the "${options.vscodeLanguage}" language.`, - ) + if (options.language) { + sections.push(`Language Preference:\nYou should always speak and think in the "${options.language}" language.`) } // Add global instructions first diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 4504cd5c01..a6220126e1 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -25,6 +25,7 @@ import { addCustomInstructions, } from "./sections" import { loadSystemPromptFile } from "./sections/custom-system-prompt" +import { formatLanguage } from "../../shared/language" async function generatePrompt( context: vscode.ExtensionContext, @@ -95,7 +96,7 @@ ${getSystemInfoSection(cwd, mode, customModeConfigs)} ${getObjectiveSection()} -${await addCustomInstructions(promptComponent?.customInstructions || modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, { vscodeLanguage: vscode.env.language, rooIgnoreInstructions })}` +${await addCustomInstructions(promptComponent?.customInstructions || modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, { language: formatLanguage(vscode.env.language), rooIgnoreInstructions })}` return basePrompt } @@ -144,7 +145,7 @@ export const SYSTEM_PROMPT = async ( globalCustomInstructions || "", cwd, mode, - { vscodeLanguage: vscode.env.language, rooIgnoreInstructions }, + { language: formatLanguage(vscode.env.language), rooIgnoreInstructions }, ) // For file-based prompts, don't include the tool sections return `${roleDefinition} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 05d9f0ba89..70b21f87bb 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -26,6 +26,7 @@ import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug, ModeConfig } from "../../shared/modes" import { checkExistKey } from "../../shared/checkExistApiConfig" import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments" +import { formatLanguage } from "../../shared/language" import { downloadTask } from "../../integrations/misc/export-markdown" import { openFile, openImage } from "../../integrations/misc/open-file" import { selectImages } from "../../integrations/misc/process-images" @@ -2358,6 +2359,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { browserToolEnabled, telemetrySetting, showRooIgnoredFiles, + language, } = await this.getState() const telemetryKey = process.env.POSTHOG_API_KEY const machineId = vscode.env.machineId @@ -2422,6 +2424,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { telemetryKey, machineId, showRooIgnoredFiles: showRooIgnoredFiles ?? true, + language, } } @@ -2556,7 +2559,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { terminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500, mode: stateValues.mode ?? defaultModeSlug, // Pass the VSCode language code directly - vscodeLanguage: vscode.env.language, + language: formatLanguage(vscode.env.language), mcpEnabled: stateValues.mcpEnabled ?? true, enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true, alwaysApproveResubmit: stateValues.alwaysApproveResubmit ?? false, diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index c35016c0a6..d2afd5fcdc 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -534,12 +534,12 @@ describe("ClineProvider", () => { expect(state).toHaveProperty("writeDelayMs") }) - test("vscodeLanguage is set to VSCode language", async () => { + test("language is set to VSCode language", async () => { // Mock VSCode language as Spanish ;(vscode.env as any).language = "es-ES" const state = await provider.getState() - expect(state.vscodeLanguage).toBe("es-ES") + expect(state.language).toBe("es-ES") }) test("diffEnabled defaults to true when not set", async () => { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 6bf436cd5d..7a4152f239 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -131,7 +131,7 @@ export interface ExtensionState { remoteBrowserHost?: string remoteBrowserEnabled?: boolean fuzzyMatchThreshold?: number - vscodeLanguage?: string + language?: string writeDelayMs: number terminalOutputLineLimit?: number mcpEnabled: boolean diff --git a/src/shared/__tests__/language.test.ts b/src/shared/__tests__/language.test.ts new file mode 100644 index 0000000000..5536d0a1f5 --- /dev/null +++ b/src/shared/__tests__/language.test.ts @@ -0,0 +1,19 @@ +import { formatLanguage } from "../language" + +describe("formatLanguage", () => { + it("should uppercase region code in locale string", () => { + expect(formatLanguage("en-us")).toBe("en-US") + expect(formatLanguage("fr-ca")).toBe("fr-CA") + expect(formatLanguage("de-de")).toBe("de-DE") + }) + + it("should return original string if no region code present", () => { + expect(formatLanguage("en")).toBe("en") + expect(formatLanguage("fr")).toBe("fr") + }) + + it("should handle empty or undefined input", () => { + expect(formatLanguage("")).toBe("en") + expect(formatLanguage(undefined as unknown as string)).toBe("en") + }) +}) diff --git a/src/shared/__tests__/modes.test.ts b/src/shared/__tests__/modes.test.ts index ce082f4730..5abcb8a8b3 100644 --- a/src/shared/__tests__/modes.test.ts +++ b/src/shared/__tests__/modes.test.ts @@ -401,7 +401,7 @@ describe("FileRestrictionError", () => { const options = { cwd: "/test/path", globalCustomInstructions: "Global instructions", - vscodeLanguage: "en", + language: "en", } await getFullModeDetails("debug", undefined, undefined, options) @@ -411,7 +411,7 @@ describe("FileRestrictionError", () => { "Global instructions", "/test/path", "debug", - { vscodeLanguage: "en" }, + { language: "en" }, ) }) diff --git a/src/shared/language.ts b/src/shared/language.ts new file mode 100644 index 0000000000..b5cb4228b5 --- /dev/null +++ b/src/shared/language.ts @@ -0,0 +1,14 @@ +/** + * Formats a VSCode locale string to ensure the region code is uppercase. + * For example, transforms "en-us" to "en-US" or "fr-ca" to "fr-CA". + * + * @param vscodeLocale - The VSCode locale string to format (e.g., "en-us", "fr-ca") + * @returns The formatted locale string with uppercase region code + */ +export function formatLanguage(vscodeLocale: string): string { + if (!vscodeLocale) { + return "en" // Default to English if no locale is provided + } + + return vscodeLocale.replace(/-(\w+)$/, (_, region) => `-${region.toUpperCase()}`) +} diff --git a/src/shared/modes.ts b/src/shared/modes.ts index 15944c615d..6fd57206bf 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -278,7 +278,7 @@ export async function getFullModeDetails( options?: { cwd?: string globalCustomInstructions?: string - vscodeLanguage?: string + language?: string }, ): Promise { // First get the base mode config from custom modes or built-in modes @@ -298,7 +298,7 @@ export async function getFullModeDetails( options.globalCustomInstructions || "", options.cwd, modeSlug, - { vscodeLanguage: options.vscodeLanguage }, + { language: options.language }, ) } diff --git a/webview-ui/jest.config.cjs b/webview-ui/jest.config.cjs index 6ee94dda39..be5b6aa1be 100644 --- a/webview-ui/jest.config.cjs +++ b/webview-ui/jest.config.cjs @@ -4,7 +4,7 @@ module.exports = { testEnvironment: "jsdom", injectGlobals: true, moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], - transform: { "^.+\\.(ts|tsx)$": ["ts-jest", { tsconfig: { jsx: "react-jsx" } }] }, + transform: { "^.+\\.(ts|tsx)$": ["ts-jest", { tsconfig: { jsx: "react-jsx", module: "ESNext" } }] }, testMatch: ["/src/**/__tests__/**/*.{js,jsx,ts,tsx}", "/src/**/*.{spec,test}.{js,jsx,ts,tsx}"], setupFilesAfterEnv: ["/src/setupTests.ts"], moduleNameMapper: { @@ -12,6 +12,12 @@ module.exports = { "^vscrui$": "/src/__mocks__/vscrui.ts", "^@vscode/webview-ui-toolkit/react$": "/src/__mocks__/@vscode/webview-ui-toolkit/react.ts", "^@/(.*)$": "/src/$1", + "^src/i18n/setup$": "/src/__mocks__/i18n/setup.ts", + "^\\.\\./setup$": "/src/__mocks__/i18n/setup.ts", + "^\\./setup$": "/src/__mocks__/i18n/setup.ts", + "^src/i18n/TranslationContext$": "/src/__mocks__/i18n/TranslationContext.tsx", + "^\\.\\./TranslationContext$": "/src/__mocks__/i18n/TranslationContext.tsx", + "^\\./TranslationContext$": "/src/__mocks__/i18n/TranslationContext.tsx" }, reporters: [["jest-simple-dot-reporter", {}]], transformIgnorePatterns: [ diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 97960851c9..38aecc9d09 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -21,6 +21,7 @@ "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@tailwindcss/vite": "^4.0.0", + "@testing-library/dom": "^10.4.0", "@vscode/webview-ui-toolkit": "^1.4.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -28,11 +29,14 @@ "debounce": "^2.1.1", "fast-deep-equal": "^3.1.3", "fzf": "^0.5.2", + "i18next": "^24.2.2", + "i18next-http-backend": "^3.0.2", "lucide-react": "^0.475.0", "mermaid": "^11.4.1", "posthog-js": "^1.227.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-i18next": "^15.4.1", "react-markdown": "^9.0.3", "react-remark": "^2.1.0", "react-textarea-autosize": "^8.5.3", @@ -78,7 +82,7 @@ "storybook": "^8.5.6", "storybook-dark-mode": "^4.0.2", "ts-jest": "^29.2.5", - "typescript": "^4.9.5", + "typescript": "^5.4.5", "vite": "6.0.11" } }, @@ -127,7 +131,6 @@ "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", @@ -445,7 +448,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -6165,26 +6167,6 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta" } }, - "node_modules/@storybook/instrumenter": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/@storybook/instrumenter/-/instrumenter-8.5.6.tgz", - "integrity": "sha512-uMOOiq/9dFoFhSl3IxuQ+yq4lClkcRtEuB6cPzD/rVCmlh+i//VkHTqFCNrDvpVA21Lsy9NLmnxLHJpBGN3Avg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@storybook/global": "^5.0.0", - "@vitest/utils": "^2.1.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^8.5.6" - } - }, "node_modules/@storybook/manager-api": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/@storybook/manager-api/-/manager-api-8.5.6.tgz", @@ -6303,96 +6285,6 @@ } } }, - "node_modules/@storybook/test": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/@storybook/test/-/test-8.5.6.tgz", - "integrity": "sha512-U4HdyAcCwc/ictwq0HWKI6j2NAUggB9ENfyH3baEWaLEI+mp4pzQMuTnOIF9TvqU7K1D5UqOyfs/hlbFxUFysg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@storybook/csf": "0.1.12", - "@storybook/global": "^5.0.0", - "@storybook/instrumenter": "8.5.6", - "@testing-library/dom": "10.4.0", - "@testing-library/jest-dom": "6.5.0", - "@testing-library/user-event": "14.5.2", - "@vitest/expect": "2.0.5", - "@vitest/spy": "2.0.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/storybook" - }, - "peerDependencies": { - "storybook": "^8.5.6" - } - }, - "node_modules/@storybook/test/node_modules/@testing-library/jest-dom": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.5.0.tgz", - "integrity": "sha512-xGGHpBXYSHUUr6XsKBfs85TWlYKpTc37cSBBVrXcib2MkHLboWlkClhWF37JKlDb9KEq3dHs+f2xR7XJEWGBxA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@adobe/css-tools": "^4.4.0", - "aria-query": "^5.0.0", - "chalk": "^3.0.0", - "css.escape": "^1.5.1", - "dom-accessibility-api": "^0.6.3", - "lodash": "^4.17.21", - "redent": "^3.0.0" - }, - "engines": { - "node": ">=14", - "npm": ">=6", - "yarn": ">=1" - } - }, - "node_modules/@storybook/test/node_modules/@testing-library/user-event": { - "version": "14.5.2", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", - "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12", - "npm": ">=6" - }, - "peerDependencies": { - "@testing-library/dom": ">=7.21.4" - } - }, - "node_modules/@storybook/test/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@storybook/test/node_modules/dom-accessibility-api": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", - "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/@storybook/theming": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-8.5.6.tgz", @@ -6644,9 +6536,6 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", - "dev": true, - "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -6666,7 +6555,6 @@ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz", "integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==", "dev": true, - "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", @@ -6708,7 +6596,6 @@ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.2.0.tgz", "integrity": "sha512-2cSskAvA1QNtKc8Y9VJQRv0tm3hLVgxRGDB+KYhIaPQJ1I+RHbhIXcM+zClKXzMes/wshsMVzf4B9vS4IZpqDQ==", "dev": true, - "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5" }, @@ -6758,10 +6645,7 @@ "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "license": "MIT", - "peer": true + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==" }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -7771,116 +7655,6 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" } }, - "node_modules/@vitest/expect": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", - "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@vitest/spy": "2.0.5", - "@vitest/utils": "2.0.5", - "chai": "^5.1.1", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/expect/node_modules/@vitest/pretty-format": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", - "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/expect/node_modules/@vitest/utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", - "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@vitest/pretty-format": "2.0.5", - "estree-walker": "^3.0.3", - "loupe": "^3.1.1", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/expect/node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", - "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "tinyspy": "^3.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, "node_modules/@vscode/webview-ui-toolkit": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@vscode/webview-ui-toolkit/-/webview-ui-toolkit-1.4.0.tgz", @@ -8007,7 +7781,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8017,7 +7790,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -8066,7 +7838,6 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, "license": "Apache-2.0", "dependencies": { "dequal": "^2.0.3" @@ -8239,18 +8010,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - } - }, "node_modules/ast-types": { "version": "0.16.1", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", @@ -8770,30 +8529,10 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -8856,18 +8595,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 16" - } - }, "node_modules/chevrotain": { "version": "11.0.3", "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", @@ -9405,7 +9132,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -9418,7 +9144,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -9568,6 +9293,14 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -10277,18 +10010,6 @@ } } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -10459,10 +10180,7 @@ "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "license": "MIT", - "peer": true + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" }, "node_modules/domexception": { "version": "4.0.0", @@ -12709,7 +12427,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -13070,6 +12787,14 @@ "dev": true, "license": "MIT" }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "dependencies": { + "void-elements": "3.1.0" + } + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -13136,6 +12861,44 @@ "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", "license": "BSD-3-Clause" }, + "node_modules/i18next": { + "version": "24.2.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.2.tgz", + "integrity": "sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "dependencies": { + "@babel/runtime": "^7.23.2" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-http-backend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz", + "integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -15602,15 +15365,6 @@ "loose-envify": "cli.js" } }, - "node_modules/loupe": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", - "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/lowlight": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", @@ -15649,9 +15403,6 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", - "dev": true, - "license": "MIT", - "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -17793,6 +17544,44 @@ "dev": true, "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -18252,18 +18041,6 @@ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==" }, - "node_modules/pathval": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", - "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -18489,7 +18266,6 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1", @@ -18504,7 +18280,6 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -18725,11 +18500,31 @@ "react": "^18.3.1" } }, + "node_modules/react-i18next": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.4.1.tgz", + "integrity": "sha512-ahGab+IaSgZmNPYXdV1n+OYky95TGpFwnKRflX/16dY04DsYYKHtVLjeny7sBSCREEcoMbAgSkFiGLF5g5Oofw==", + "dependencies": { + "@babel/runtime": "^7.25.0", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, "license": "MIT" }, "node_modules/react-markdown": { @@ -20671,7 +20466,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -20783,30 +20577,6 @@ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==" }, - "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -21141,17 +20911,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "license": "Apache-2.0", + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", + "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/ufo": { @@ -21742,6 +21511,14 @@ } } }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/vscode-jsonrpc": { "version": "8.2.0", "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", diff --git a/webview-ui/package.json b/webview-ui/package.json index 589ccbf674..29dad1893a 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -28,6 +28,7 @@ "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@tailwindcss/vite": "^4.0.0", + "@testing-library/dom": "^10.4.0", "@vscode/webview-ui-toolkit": "^1.4.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -35,11 +36,14 @@ "debounce": "^2.1.1", "fast-deep-equal": "^3.1.3", "fzf": "^0.5.2", + "i18next": "^24.2.2", + "i18next-http-backend": "^3.0.2", "lucide-react": "^0.475.0", "mermaid": "^11.4.1", "posthog-js": "^1.227.2", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-i18next": "^15.4.1", "react-markdown": "^9.0.3", "react-remark": "^2.1.0", "react-textarea-autosize": "^8.5.3", @@ -85,7 +89,7 @@ "storybook": "^8.5.6", "storybook-dark-mode": "^4.0.2", "ts-jest": "^29.2.5", - "typescript": "^4.9.5", + "typescript": "^5.4.5", "vite": "6.0.11" } } diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 389f5709fc..9ce93b9a0e 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -1,8 +1,8 @@ import { useCallback, useEffect, useRef, useState } from "react" import { useEvent } from "react-use" - import { ExtensionMessage } from "../../src/shared/ExtensionMessage" import { ShowHumanRelayDialogMessage } from "../../src/shared/ExtensionMessage" +import TranslationProvider from "./i18n/TranslationContext" import { vscode } from "./utils/vscode" import { telemetryClient } from "./utils/TelemetryClient" @@ -131,7 +131,9 @@ const App = () => { const AppWithProviders = () => ( - + + + ) diff --git a/webview-ui/src/__mocks__/i18n/TranslationContext.tsx b/webview-ui/src/__mocks__/i18n/TranslationContext.tsx new file mode 100644 index 0000000000..1b838923bc --- /dev/null +++ b/webview-ui/src/__mocks__/i18n/TranslationContext.tsx @@ -0,0 +1,47 @@ +import React, { ReactNode } from "react" +import i18next from "./setup" + +// Create a mock context +export const TranslationContext = React.createContext<{ + t: (key: string, options?: Record) => string + i18n: typeof i18next +}>({ + t: (key: string, options?: Record) => { + // Handle specific test cases + if (key === "settings.autoApprove.title") { + return "Auto-Approve" + } + if (key === "notifications.error" && options?.message) { + return `Operation failed: ${options.message}` + } + return key // Default fallback + }, + i18n: i18next, +}) + +// Mock translation provider +export const TranslationProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + return ( + ) => { + // Handle specific test cases + if (key === "settings.autoApprove.title") { + return "Auto-Approve" + } + if (key === "notifications.error" && options?.message) { + return `Operation failed: ${options.message}` + } + return key // Default fallback + }, + i18n: i18next, + }}> + {children} + + ) +} + +// Custom hook for easy translations +export const useAppTranslation = () => React.useContext(TranslationContext) + +export default TranslationProvider diff --git a/webview-ui/src/__mocks__/i18n/setup.ts b/webview-ui/src/__mocks__/i18n/setup.ts new file mode 100644 index 0000000000..6ba0cf7e38 --- /dev/null +++ b/webview-ui/src/__mocks__/i18n/setup.ts @@ -0,0 +1,62 @@ +import i18next from "i18next" +import { initReactI18next } from "react-i18next" + +// Mock translations for testing +const translations: Record> = { + en: { + chat: { + greeting: "What can Roo do for you?", + }, + settings: { + autoApprove: { + title: "Auto-Approve", + }, + }, + common: { + notifications: { + error: "Operation failed: {{message}}", + }, + }, + }, + es: { + chat: { + greeting: "¿Qué puede hacer Roo por ti?", + }, + }, +} + +// Initialize i18next for React +i18next.use(initReactI18next).init({ + lng: "en", + fallbackLng: "en", + debug: false, + interpolation: { + escapeValue: false, + }, + resources: { + en: { + chat: translations.en.chat, + settings: translations.en.settings, + common: translations.en.common, + }, + es: { + chat: translations.es.chat, + }, + }, +}) + +export function loadTranslations() { + // Translations are already loaded in the mock +} + +export function addTranslation(language: string, namespace: string, resources: any) { + if (!translations[language]) { + translations[language] = {} + } + translations[language][namespace] = resources + + // Also add to i18next + i18next.addResourceBundle(language, namespace, resources, true, true) +} + +export default i18next diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 09dcdd3ca1..46958e738d 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -30,6 +30,7 @@ import { AudioType } from "../../../../src/shared/WebviewMessage" import { validateCommand } from "../../utils/command-validation" import { getAllModes } from "../../../../src/shared/modes" import TelemetryBanner from "../common/TelemetryBanner" +import { useAppTranslation } from "@/i18n/TranslationContext" interface ChatViewProps { isHidden: boolean @@ -66,6 +67,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie telemetrySetting, } = useExtensionState() + const { t } = useAppTranslation() + //const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort) const modifiedMessages = useMemo(() => combineApiRequests(combineCommandSequences(messages.slice(1))), [messages]) @@ -1100,7 +1103,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie {telemetrySetting === "unset" && } {showAnnouncement && }
-

What can Roo do for you?

+

{t("chat:greeting")}

Thanks to the latest breakthroughs in agentic coding capabilities, I can handle complex software development tasks step-by-step. With tools that let me create & edit files, explore diff --git a/webview-ui/src/components/ui/combobox-primitive.tsx b/webview-ui/src/components/ui/combobox-primitive.tsx index 13bad87aba..b2c3104a35 100644 --- a/webview-ui/src/components/ui/combobox-primitive.tsx +++ b/webview-ui/src/components/ui/combobox-primitive.tsx @@ -50,7 +50,7 @@ export type ComboboxType = "single" | "multiple" export interface ComboboxBaseProps extends React.ComponentProps, - Omit, "value" | "defaultValue" | "onValueChange"> { + Omit, "value" | "defaultValue" | "onValueChange" | "children"> { type?: ComboboxType | undefined inputValue?: string defaultInputValue?: string diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index f7292e2721..d79f263e83 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -116,7 +116,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode enableCheckpoints: true, checkpointStorage: "task", fuzzyMatchThreshold: 1.0, - vscodeLanguage: "en", // Default language code + language: "en", // Default language code enableCustomModeCreation: true, writeDelayMs: 1000, browserViewportSize: "900x600", diff --git a/webview-ui/src/i18n/TranslationContext.tsx b/webview-ui/src/i18n/TranslationContext.tsx new file mode 100644 index 0000000000..e6115dae6a --- /dev/null +++ b/webview-ui/src/i18n/TranslationContext.tsx @@ -0,0 +1,51 @@ +import React, { createContext, useContext, ReactNode, useEffect } from "react" +import { useTranslation } from "react-i18next" +import i18next, { loadTranslations } from "./setup" +import { useExtensionState } from "@/context/ExtensionStateContext" + +// Create context for translations +export const TranslationContext = createContext<{ + t: (key: string, options?: Record) => string + i18n: typeof i18next +}>({ + t: (key: string) => key, + i18n: i18next, +}) + +// Translation provider component +export const TranslationProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + // Initialize with default configuration + const { i18n } = useTranslation() + // Get the extension state directly - it already contains all state properties + const extensionState = useExtensionState() + + // Load translations once when the component mounts + useEffect(() => { + try { + loadTranslations() + } catch (error) { + console.error("Failed to load translations:", error) + } + }, []) + + useEffect(() => { + i18n.changeLanguage(extensionState.language) + }, [i18n, extensionState.language]) + + return ( + ) => { + return i18n.t(key, options) + }, + i18n, + }}> + {children} + + ) +} + +// Custom hook for easy translations +export const useAppTranslation = () => useContext(TranslationContext) + +export default TranslationProvider diff --git a/webview-ui/src/i18n/__tests__/TranslationContext.test.tsx b/webview-ui/src/i18n/__tests__/TranslationContext.test.tsx new file mode 100644 index 0000000000..0b4dbf8238 --- /dev/null +++ b/webview-ui/src/i18n/__tests__/TranslationContext.test.tsx @@ -0,0 +1,52 @@ +import React from "react" +import { render } from "@testing-library/react" +import "@testing-library/jest-dom" +import TranslationProvider, { useAppTranslation } from "../TranslationContext" +import { setupI18nForTests } from "../test-utils" + +// Mock the useExtensionState hook +jest.mock("@/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + language: "en", + }), +})) + +// Mock component that uses the translation context +const TestComponent = () => { + const { t } = useAppTranslation() + return ( +

+

{t("settings.autoApprove.title")}

+

{t("notifications.error", { message: "Test error" })}

+
+ ) +} + +describe("TranslationContext", () => { + beforeAll(() => { + // Initialize i18next with test translations + setupI18nForTests() + }) + + it("should provide translations via context", () => { + const { getByTestId } = render( + + + , + ) + + // Check if translation is provided correctly + expect(getByTestId("translation-test")).toHaveTextContent("Auto-Approve") + }) + + it("should handle interpolation correctly", () => { + const { getByTestId } = render( + + + , + ) + + // Check if interpolation works + expect(getByTestId("translation-interpolation")).toHaveTextContent("Operation failed: Test error") + }) +}) diff --git a/webview-ui/src/i18n/locales/ar/chat.json b/webview-ui/src/i18n/locales/ar/chat.json new file mode 100644 index 0000000000..af09e7cf5e --- /dev/null +++ b/webview-ui/src/i18n/locales/ar/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "ماذا يمكن أن يفعل Roo من أجلك؟" +} diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json new file mode 100644 index 0000000000..9f9d819936 --- /dev/null +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Què pot fer Roo per tu?" +} diff --git a/webview-ui/src/i18n/locales/cs/chat.json b/webview-ui/src/i18n/locales/cs/chat.json new file mode 100644 index 0000000000..989655bf4b --- /dev/null +++ b/webview-ui/src/i18n/locales/cs/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Co pro vás může Roo udělat?" +} diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json new file mode 100644 index 0000000000..9047746c50 --- /dev/null +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Was kann Roo für dich tun?" +} diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json new file mode 100644 index 0000000000..1b742bb11f --- /dev/null +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "What can Roo do for you?" +} diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json new file mode 100644 index 0000000000..34b857fba7 --- /dev/null +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "¿Qué puedo hacer por ti?" +} diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json new file mode 100644 index 0000000000..8a2e696594 --- /dev/null +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Que peut faire Roo pour vous ?" +} diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json new file mode 100644 index 0000000000..5212c2026c --- /dev/null +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Roo आपके लिए क्या कर सकता है?" +} diff --git a/webview-ui/src/i18n/locales/hu/chat.json b/webview-ui/src/i18n/locales/hu/chat.json new file mode 100644 index 0000000000..f57221300c --- /dev/null +++ b/webview-ui/src/i18n/locales/hu/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Mit tehet érted a Roo?" +} diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json new file mode 100644 index 0000000000..6ab48932bc --- /dev/null +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Cosa può fare Roo per te?" +} diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json new file mode 100644 index 0000000000..d372814cee --- /dev/null +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Rooは何をお手伝いできますか?" +} diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json new file mode 100644 index 0000000000..a96e6d17a3 --- /dev/null +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Roo가 당신을 위해 무엇을 할 수 있을까요?" +} diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json new file mode 100644 index 0000000000..b21e0047ed --- /dev/null +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Co Roo może dla Ciebie zrobić?" +} diff --git a/webview-ui/src/i18n/locales/pt-br/chat.json b/webview-ui/src/i18n/locales/pt-br/chat.json new file mode 100644 index 0000000000..9fcd1ad1e7 --- /dev/null +++ b/webview-ui/src/i18n/locales/pt-br/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "O que o Roo pode fazer por você?" +} diff --git a/webview-ui/src/i18n/locales/pt/chat.json b/webview-ui/src/i18n/locales/pt/chat.json new file mode 100644 index 0000000000..66bef0dff9 --- /dev/null +++ b/webview-ui/src/i18n/locales/pt/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "O que o Roo pode fazer por si?" +} diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json new file mode 100644 index 0000000000..b01532b168 --- /dev/null +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Что Roo может сделать для вас?" +} diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json new file mode 100644 index 0000000000..8c160b2b77 --- /dev/null +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Roo sizin için ne yapabilir?" +} diff --git a/webview-ui/src/i18n/locales/zh-cn/chat.json b/webview-ui/src/i18n/locales/zh-cn/chat.json new file mode 100644 index 0000000000..4e17fd3ca0 --- /dev/null +++ b/webview-ui/src/i18n/locales/zh-cn/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Roo能为您做什么?" +} diff --git a/webview-ui/src/i18n/locales/zh-tw/chat.json b/webview-ui/src/i18n/locales/zh-tw/chat.json new file mode 100644 index 0000000000..13724748a9 --- /dev/null +++ b/webview-ui/src/i18n/locales/zh-tw/chat.json @@ -0,0 +1,3 @@ +{ + "greeting": "Roo能為您做什麼?" +} diff --git a/webview-ui/src/i18n/setup.ts b/webview-ui/src/i18n/setup.ts new file mode 100644 index 0000000000..678cdc1d49 --- /dev/null +++ b/webview-ui/src/i18n/setup.ts @@ -0,0 +1,54 @@ +import i18next from "i18next" +import { initReactI18next } from "react-i18next" + +// Build translations object +const translations: Record> = {} + +// Dynamically load locale files +const localeFiles = import.meta.glob("./locales/**/*.json", { eager: true }) + +// Process all locale files +Object.entries(localeFiles).forEach(([path, module]) => { + // Extract language and namespace from path + // Example path: './locales/en/common.json' -> language: 'en', namespace: 'common' + const match = path.match(/\.\/locales\/([^/]+)\/([^/]+)\.json/) + + if (match) { + const [, language, namespace] = match + + // Initialize language object if it doesn't exist + if (!translations[language]) { + translations[language] = {} + } + + // Add namespace resources to language + translations[language][namespace] = (module as any).default || module + } +}) + +console.log("Dynamically loaded translations:", Object.keys(translations)) + +// Initialize i18next for React +// This will be initialized with the VSCode language in TranslationProvider +i18next.use(initReactI18next).init({ + lng: "en", // Default language (will be overridden) + fallbackLng: "en", + debug: false, + interpolation: { + escapeValue: false, // React already escapes by default + }, +}) + +export function loadTranslations() { + Object.entries(translations).forEach(([lang, namespaces]) => { + try { + Object.entries(namespaces).forEach(([namespace, resources]) => { + i18next.addResourceBundle(lang, namespace, resources, true, true) + }) + } catch (error) { + console.warn(`Could not load ${lang} translations:`, error) + } + }) +} + +export default i18next diff --git a/webview-ui/src/i18n/test-utils.ts b/webview-ui/src/i18n/test-utils.ts new file mode 100644 index 0000000000..9abd4d9e06 --- /dev/null +++ b/webview-ui/src/i18n/test-utils.ts @@ -0,0 +1,37 @@ +import i18next from "i18next" +import { initReactI18next } from "react-i18next" + +/** + * Sets up i18next for testing with pre-defined translations. + * Use this in test files to ensure consistent translation handling. + */ +export const setupI18nForTests = () => { + i18next.use(initReactI18next).init({ + lng: "en", + fallbackLng: "en", + debug: false, + interpolation: { + escapeValue: false, + }, + // Pre-define all translations needed for tests + resources: { + en: { + settings: { + autoApprove: { + title: "Auto-Approve", + }, + }, + common: { + notifications: { + error: "Operation failed: {{message}}", + }, + }, + chat: { + test: "Test", + }, + }, + }, + }) + + return i18next +} From 28e745124a55f84dcb6fd2e9d19d4525c4f35a4a Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Tue, 11 Mar 2025 22:34:39 -0400 Subject: [PATCH 9/9] PR cleanup --- .roomodes | 4 +- .../__snapshots__/system.test.ts.snap | 30 +++- .../prompts/sections/custom-instructions.ts | 4 +- src/core/prompts/sections/rules.ts | 1 - src/core/prompts/system.ts | 21 +-- src/core/prompts/tools/index.ts | 4 +- webview-ui/.gitignore | 2 + webview-ui/package-lock.json | 26 ++- webview-ui/package.json | 1 - .../src/components/prompts/PromptsView.tsx | 150 +++++++++--------- webview-ui/src/i18n/TranslationContext.tsx | 14 +- webview-ui/src/i18n/locales/ar/.gitkeep | 0 webview-ui/src/i18n/locales/ar/chat.json | 3 - webview-ui/src/i18n/locales/ca/.gitkeep | 0 webview-ui/src/i18n/locales/ca/chat.json | 3 - webview-ui/src/i18n/locales/cs/.gitkeep | 0 webview-ui/src/i18n/locales/cs/chat.json | 3 - webview-ui/src/i18n/locales/de/.gitkeep | 0 webview-ui/src/i18n/locales/de/chat.json | 3 - webview-ui/src/i18n/locales/en/.gitkeep | 0 webview-ui/src/i18n/locales/es/.gitkeep | 0 webview-ui/src/i18n/locales/es/chat.json | 3 - webview-ui/src/i18n/locales/fr/.gitkeep | 0 webview-ui/src/i18n/locales/fr/chat.json | 3 - webview-ui/src/i18n/locales/hi/.gitkeep | 0 webview-ui/src/i18n/locales/hi/chat.json | 3 - webview-ui/src/i18n/locales/hu/.gitkeep | 0 webview-ui/src/i18n/locales/hu/chat.json | 3 - webview-ui/src/i18n/locales/it/.gitkeep | 0 webview-ui/src/i18n/locales/it/chat.json | 3 - webview-ui/src/i18n/locales/ja/.gitkeep | 0 webview-ui/src/i18n/locales/ja/chat.json | 3 - webview-ui/src/i18n/locales/ko/.gitkeep | 0 webview-ui/src/i18n/locales/ko/chat.json | 3 - webview-ui/src/i18n/locales/pl/.gitkeep | 0 webview-ui/src/i18n/locales/pl/chat.json | 3 - webview-ui/src/i18n/locales/pt-BR/.gitkeep | 0 webview-ui/src/i18n/locales/pt-br/chat.json | 3 - webview-ui/src/i18n/locales/pt/.gitkeep | 0 webview-ui/src/i18n/locales/pt/chat.json | 3 - webview-ui/src/i18n/locales/ru/.gitkeep | 0 webview-ui/src/i18n/locales/ru/chat.json | 3 - webview-ui/src/i18n/locales/tr/.gitkeep | 0 webview-ui/src/i18n/locales/tr/chat.json | 3 - webview-ui/src/i18n/locales/zh-CN/.gitkeep | 0 webview-ui/src/i18n/locales/zh-TW/.gitkeep | 0 webview-ui/src/i18n/locales/zh-cn/chat.json | 3 - webview-ui/src/i18n/locales/zh-tw/chat.json | 3 - 48 files changed, 152 insertions(+), 159 deletions(-) create mode 100644 webview-ui/src/i18n/locales/ar/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/ar/chat.json create mode 100644 webview-ui/src/i18n/locales/ca/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/ca/chat.json create mode 100644 webview-ui/src/i18n/locales/cs/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/cs/chat.json create mode 100644 webview-ui/src/i18n/locales/de/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/de/chat.json create mode 100644 webview-ui/src/i18n/locales/en/.gitkeep create mode 100644 webview-ui/src/i18n/locales/es/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/es/chat.json create mode 100644 webview-ui/src/i18n/locales/fr/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/fr/chat.json create mode 100644 webview-ui/src/i18n/locales/hi/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/hi/chat.json create mode 100644 webview-ui/src/i18n/locales/hu/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/hu/chat.json create mode 100644 webview-ui/src/i18n/locales/it/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/it/chat.json create mode 100644 webview-ui/src/i18n/locales/ja/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/ja/chat.json create mode 100644 webview-ui/src/i18n/locales/ko/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/ko/chat.json create mode 100644 webview-ui/src/i18n/locales/pl/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/pl/chat.json create mode 100644 webview-ui/src/i18n/locales/pt-BR/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/pt-br/chat.json create mode 100644 webview-ui/src/i18n/locales/pt/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/pt/chat.json create mode 100644 webview-ui/src/i18n/locales/ru/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/ru/chat.json create mode 100644 webview-ui/src/i18n/locales/tr/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/tr/chat.json create mode 100644 webview-ui/src/i18n/locales/zh-CN/.gitkeep create mode 100644 webview-ui/src/i18n/locales/zh-TW/.gitkeep delete mode 100644 webview-ui/src/i18n/locales/zh-cn/chat.json delete mode 100644 webview-ui/src/i18n/locales/zh-tw/chat.json diff --git a/.roomodes b/.roomodes index 0ccbb47432..f10ca32056 100644 --- a/.roomodes +++ b/.roomodes @@ -1,8 +1,8 @@ { "customModes": [ { - "slug": "translator", - "name": "Translator", + "slug": "translate", + "name": "Translate", "roleDefinition": "You are Roo, a linguistic specialist focused on translating and managing localization files. Your responsibility is to help maintain and update translation files for the application, ensuring consistency and accuracy across all language resources.", "groups": [ "read", diff --git a/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap b/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap index 2668f15ead..0202ebcef7 100644 --- a/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap +++ b/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap @@ -132,12 +132,14 @@ Example: Requesting to write to frontend-config.json ## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: \`touch ./testdata/example.file\`, \`dir ./examples/model1/data/yaml\`, or \`go test ./cmd/front --config ./cmd/front/config.yml\`. If directed by the user, you may open a terminal in a different directory by using the \`cwd\` parameter. Parameters: - command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. +- cwd: (optional) The working directory to execute the command in (default: /test/path) Usage: Your command here +Working directory path (optional) Example: Requesting to execute npm run dev @@ -145,6 +147,12 @@ Example: Requesting to execute npm run dev npm run dev +Example: Requesting to execute ls in a specific directory if directed + +ls -la +/home/user/projects + + ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. Parameters: @@ -543,12 +551,14 @@ Example: Replace all occurrences of "old" with "new" using regex ## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: \`touch ./testdata/example.file\`, \`dir ./examples/model1/data/yaml\`, or \`go test ./cmd/front --config ./cmd/front/config.yml\`. If directed by the user, you may open a terminal in a different directory by using the \`cwd\` parameter. Parameters: - command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. +- cwd: (optional) The working directory to execute the command in (default: /test/path) Usage: Your command here +Working directory path (optional) Example: Requesting to execute npm run dev @@ -556,6 +566,12 @@ Example: Requesting to execute npm run dev npm run dev +Example: Requesting to execute ls in a specific directory if directed + +ls -la +/home/user/projects + + ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. Parameters: @@ -923,12 +939,14 @@ Example: Replace all occurrences of "old" with "new" using regex ## execute_command -Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Commands will be executed in the current working directory: /test/path +Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: \`touch ./testdata/example.file\`, \`dir ./examples/model1/data/yaml\`, or \`go test ./cmd/front --config ./cmd/front/config.yml\`. If directed by the user, you may open a terminal in a different directory by using the \`cwd\` parameter. Parameters: - command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions. +- cwd: (optional) The working directory to execute the command in (default: /test/path) Usage: Your command here +Working directory path (optional) Example: Requesting to execute npm run dev @@ -936,6 +954,12 @@ Example: Requesting to execute npm run dev npm run dev +Example: Requesting to execute ls in a specific directory if directed + +ls -la +/home/user/projects + + ## ask_followup_question Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth. Parameters: diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts index b50219d563..04ef5b06c9 100644 --- a/src/core/prompts/sections/custom-instructions.ts +++ b/src/core/prompts/sections/custom-instructions.ts @@ -47,7 +47,9 @@ export async function addCustomInstructions( // Add language preference if provided if (options.language) { - sections.push(`Language Preference:\nYou should always speak and think in the "${options.language}" language.`) + sections.push( + `Language Preference:\nYou should always speak and think in the "${options.language}" language unless the user gives you instructions below to do otherwise.`, + ) } // Add global instructions first diff --git a/src/core/prompts/sections/rules.ts b/src/core/prompts/sections/rules.ts index 99979544bf..86e554a157 100644 --- a/src/core/prompts/sections/rules.ts +++ b/src/core/prompts/sections/rules.ts @@ -16,7 +16,6 @@ function getEditingInstructions(diffStrategy?: DiffStrategy, experiments?: Recor } else { availableTools.push("write_to_file (for creating new files or complete file rewrites)") } - if (experiments?.["insert_content"]) { availableTools.push("insert_content (for adding lines to existing files)") } diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index a6220126e1..41c0f4fc4a 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -39,7 +39,7 @@ async function generatePrompt( customModeConfigs?: ModeConfig[], globalCustomInstructions?: string, diffEnabled?: boolean, - experiments?: Record | boolean | undefined, + experiments?: Record, enableMcpServerCreation?: boolean, rooIgnoreInstructions?: string, ): Promise { @@ -61,12 +61,6 @@ async function generatePrompt( : Promise.resolve(""), ]) - // Convert experiments to an object if it's a boolean - const experimentSettings = - typeof experiments === "boolean" - ? {} // Empty object if experiments is just a boolean - : (experiments ?? {}) - const basePrompt = `${roleDefinition} ${getSharedToolUseSection()} @@ -79,7 +73,7 @@ ${getToolDescriptionsForMode( browserViewportSize, mcpHub, customModeConfigs, - experimentSettings, + experiments, )} ${getToolUseGuidelinesSection()} @@ -90,7 +84,7 @@ ${getCapabilitiesSection(cwd, supportsComputerUse, mcpHub, effectiveDiffStrategy ${modesSection} -${getRulesSection(cwd, supportsComputerUse, effectiveDiffStrategy, experimentSettings)} +${getRulesSection(cwd, supportsComputerUse, effectiveDiffStrategy, experiments)} ${getSystemInfoSection(cwd, mode, customModeConfigs)} @@ -113,7 +107,7 @@ export const SYSTEM_PROMPT = async ( customModes?: ModeConfig[], globalCustomInstructions?: string, diffEnabled?: boolean, - experiments?: Record | boolean | undefined, + experiments?: Record, enableMcpServerCreation?: boolean, rooIgnoreInstructions?: string, ): Promise => { @@ -157,11 +151,6 @@ ${customInstructions}` // If diff is disabled, don't pass the diffStrategy const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined - // Convert experiments to an object if it's a boolean - const experimentSettings = - typeof experiments === "boolean" - ? {} // Empty object if experiments is just a boolean - : (experiments ?? {}) return generatePrompt( context, @@ -175,7 +164,7 @@ ${customInstructions}` customModes, globalCustomInstructions, diffEnabled, - experimentSettings, + experiments, enableMcpServerCreation, rooIgnoreInstructions, ) diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index 2aec9bfda1..1b9b9a43d9 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -61,15 +61,13 @@ export function getToolDescriptionsForMode( const tools = new Set() - const experimentSettings = experiments ?? {} - // Add tools from mode's groups config.groups.forEach((groupEntry) => { const groupName = getGroupName(groupEntry) const toolGroup = TOOL_GROUPS[groupName] if (toolGroup) { toolGroup.tools.forEach((tool) => { - if (isToolAllowedForMode(tool as ToolName, mode, customModes ?? [], experimentSettings)) { + if (isToolAllowedForMode(tool as ToolName, mode, customModes ?? [], experiments ?? {})) { tools.add(tool) } }) diff --git a/webview-ui/.gitignore b/webview-ui/.gitignore index e2d1897154..1f81cba3f5 100644 --- a/webview-ui/.gitignore +++ b/webview-ui/.gitignore @@ -23,3 +23,5 @@ yarn-debug.log* yarn-error.log* *storybook.log + +tsconfig.tsbuildinfo diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json index 38aecc9d09..439e5bd797 100644 --- a/webview-ui/package-lock.json +++ b/webview-ui/package-lock.json @@ -21,7 +21,6 @@ "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@tailwindcss/vite": "^4.0.0", - "@testing-library/dom": "^10.4.0", "@vscode/webview-ui-toolkit": "^1.4.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -131,6 +130,7 @@ "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", @@ -448,6 +448,7 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -6536,6 +6537,8 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -6645,7 +6648,9 @@ "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==" + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -7781,6 +7786,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -7790,6 +7796,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -7838,6 +7845,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, "license": "Apache-2.0", "dependencies": { "dequal": "^2.0.3" @@ -8533,6 +8541,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -9132,6 +9141,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -9144,6 +9154,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -10180,7 +10191,9 @@ "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", - "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==" + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "peer": true }, "node_modules/domexception": { "version": "4.0.0", @@ -12427,6 +12440,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -15403,6 +15417,8 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -18266,6 +18282,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1", @@ -18280,6 +18297,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -18525,6 +18543,7 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, "license": "MIT" }, "node_modules/react-markdown": { @@ -20466,6 +20485,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" diff --git a/webview-ui/package.json b/webview-ui/package.json index 29dad1893a..264878328c 100644 --- a/webview-ui/package.json +++ b/webview-ui/package.json @@ -28,7 +28,6 @@ "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", "@tailwindcss/vite": "^4.0.0", - "@testing-library/dom": "^10.4.0", "@vscode/webview-ui-toolkit": "^1.4.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", diff --git a/webview-ui/src/components/prompts/PromptsView.tsx b/webview-ui/src/components/prompts/PromptsView.tsx index 483cfa6b27..ef1282b4c2 100644 --- a/webview-ui/src/components/prompts/PromptsView.tsx +++ b/webview-ui/src/components/prompts/PromptsView.tsx @@ -30,6 +30,7 @@ import { import { TOOL_GROUPS, GROUP_DISPLAY_NAMES, ToolGroup } from "../../../../src/shared/tool-groups" import { vscode } from "../../utils/vscode" import { Tab, TabContent, TabHeader } from "../common/Tab" +import i18next from "i18next" // Get all available groups that should show in prompts view const availableGroups = (Object.keys(TOOL_GROUPS) as ToolGroup[]).filter((group) => !TOOL_GROUPS[group].alwaysAvailable) @@ -409,49 +410,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => { -
-
Custom Instructions for All Modes
-
- These instructions apply to all modes. They provide a base set of behaviors that can be enhanced - by mode-specific instructions below. -
- { - const value = - (e as CustomEvent)?.detail?.target?.value || - ((e as any).target as HTMLTextAreaElement).value - setCustomInstructions(value || undefined) - vscode.postMessage({ - type: "customInstructions", - text: value.trim() || undefined, - }) - }} - rows={4} - resize="vertical" - className="w-full" - data-testid="global-custom-instructions-textarea" - /> -
- Instructions can also be loaded from{" "} - - vscode.postMessage({ - type: "openFile", - text: "./.clinerules", - values: { - create: true, - content: "", - }, - }) - }> - .clinerules - {" "} - in your workspace. -
-
-
+
e.stopPropagation()} className="flex justify-between items-center mb-3">

Modes

@@ -882,8 +841,37 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
+ {/* + NOTE: This setting is placed in PromptsView rather than SettingsView since it + directly affects the functionality related to modes and custom mode creation, + which are managed in this component. This is an intentional deviation from + the standard pattern described in cline_docs/settings.md. + */} +
+ { + // Just update the local state through React context + // The React context will update the global state + setEnableCustomModeCreation(e.target.checked) + }}> + Enable Custom Mode Creation Through Prompts + +

+ When enabled, Roo allows you to create custom modes using prompts like ‘Make me a custom + mode that…’. Disabling this reduces your system prompt by about 700 tokens when this feature + isn’t needed. When disabled you can still manually create custom modes using the + button + above or by editing the related config JSON. +

+
+ {/* Custom System Prompt Disclosure */} -
+
)}
+
- {/* - NOTE: This setting is placed in PromptsView rather than SettingsView since it - directly affects the functionality related to modes and custom mode creation, - which are managed in this component. This is an intentional deviation from - the standard pattern described in cline_docs/settings.md. - */} -
- { - // Just update the local state through React context - // The React context will update the global state - setEnableCustomModeCreation(e.target.checked) - }}> - Enable Custom Mode Creation Through Prompts - -

- When enabled, Roo allows you to create custom modes using prompts like ‘Make me a custom - mode that…’. Disabling this reduces your system prompt by about 700 tokens when this feature - isn’t needed. When disabled you can still manually create custom modes using the + button - above or by editing the related config JSON. -

+
+

+ Custom Instructions for All Modes +

+ +
+ These instructions apply to all modes. They provide a base set of behaviors that can be enhanced + by mode-specific instructions below. +
+ If you would like Roo to think and speak in a different language than your editor display + language ({i18next.language}), you can specify it here. +
+ { + const value = + (e as CustomEvent)?.detail?.target?.value || + ((e as any).target as HTMLTextAreaElement).value + setCustomInstructions(value || undefined) + vscode.postMessage({ + type: "customInstructions", + text: value.trim() || undefined, + }) + }} + rows={4} + resize="vertical" + className="w-full" + data-testid="global-custom-instructions-textarea" + /> +
+ Instructions can also be loaded from{" "} + + vscode.postMessage({ + type: "openFile", + text: "./.clinerules", + values: { + create: true, + content: "", + }, + }) + }> + .clinerules + {" "} + in your workspace.
diff --git a/webview-ui/src/i18n/TranslationContext.tsx b/webview-ui/src/i18n/TranslationContext.tsx index e6115dae6a..17f1545065 100644 --- a/webview-ui/src/i18n/TranslationContext.tsx +++ b/webview-ui/src/i18n/TranslationContext.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext, ReactNode, useEffect } from "react" +import React, { createContext, useContext, ReactNode, useEffect, useCallback } from "react" import { useTranslation } from "react-i18next" import i18next, { loadTranslations } from "./setup" import { useExtensionState } from "@/context/ExtensionStateContext" @@ -32,12 +32,18 @@ export const TranslationProvider: React.FC<{ children: ReactNode }> = ({ childre i18n.changeLanguage(extensionState.language) }, [i18n, extensionState.language]) + // Memoize the translation function to prevent unnecessary re-renders + const translate = useCallback( + (key: string, options?: Record) => { + return i18n.t(key, options) + }, + [i18n], + ) + return ( ) => { - return i18n.t(key, options) - }, + t: translate, i18n, }}> {children} diff --git a/webview-ui/src/i18n/locales/ar/.gitkeep b/webview-ui/src/i18n/locales/ar/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/ar/chat.json b/webview-ui/src/i18n/locales/ar/chat.json deleted file mode 100644 index af09e7cf5e..0000000000 --- a/webview-ui/src/i18n/locales/ar/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "ماذا يمكن أن يفعل Roo من أجلك؟" -} diff --git a/webview-ui/src/i18n/locales/ca/.gitkeep b/webview-ui/src/i18n/locales/ca/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json deleted file mode 100644 index 9f9d819936..0000000000 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "Què pot fer Roo per tu?" -} diff --git a/webview-ui/src/i18n/locales/cs/.gitkeep b/webview-ui/src/i18n/locales/cs/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/cs/chat.json b/webview-ui/src/i18n/locales/cs/chat.json deleted file mode 100644 index 989655bf4b..0000000000 --- a/webview-ui/src/i18n/locales/cs/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "Co pro vás může Roo udělat?" -} diff --git a/webview-ui/src/i18n/locales/de/.gitkeep b/webview-ui/src/i18n/locales/de/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json deleted file mode 100644 index 9047746c50..0000000000 --- a/webview-ui/src/i18n/locales/de/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "Was kann Roo für dich tun?" -} diff --git a/webview-ui/src/i18n/locales/en/.gitkeep b/webview-ui/src/i18n/locales/en/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/es/.gitkeep b/webview-ui/src/i18n/locales/es/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json deleted file mode 100644 index 34b857fba7..0000000000 --- a/webview-ui/src/i18n/locales/es/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "¿Qué puedo hacer por ti?" -} diff --git a/webview-ui/src/i18n/locales/fr/.gitkeep b/webview-ui/src/i18n/locales/fr/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json deleted file mode 100644 index 8a2e696594..0000000000 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "Que peut faire Roo pour vous ?" -} diff --git a/webview-ui/src/i18n/locales/hi/.gitkeep b/webview-ui/src/i18n/locales/hi/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json deleted file mode 100644 index 5212c2026c..0000000000 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "Roo आपके लिए क्या कर सकता है?" -} diff --git a/webview-ui/src/i18n/locales/hu/.gitkeep b/webview-ui/src/i18n/locales/hu/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/hu/chat.json b/webview-ui/src/i18n/locales/hu/chat.json deleted file mode 100644 index f57221300c..0000000000 --- a/webview-ui/src/i18n/locales/hu/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "Mit tehet érted a Roo?" -} diff --git a/webview-ui/src/i18n/locales/it/.gitkeep b/webview-ui/src/i18n/locales/it/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json deleted file mode 100644 index 6ab48932bc..0000000000 --- a/webview-ui/src/i18n/locales/it/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "Cosa può fare Roo per te?" -} diff --git a/webview-ui/src/i18n/locales/ja/.gitkeep b/webview-ui/src/i18n/locales/ja/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json deleted file mode 100644 index d372814cee..0000000000 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "Rooは何をお手伝いできますか?" -} diff --git a/webview-ui/src/i18n/locales/ko/.gitkeep b/webview-ui/src/i18n/locales/ko/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json deleted file mode 100644 index a96e6d17a3..0000000000 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "Roo가 당신을 위해 무엇을 할 수 있을까요?" -} diff --git a/webview-ui/src/i18n/locales/pl/.gitkeep b/webview-ui/src/i18n/locales/pl/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json deleted file mode 100644 index b21e0047ed..0000000000 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "Co Roo może dla Ciebie zrobić?" -} diff --git a/webview-ui/src/i18n/locales/pt-BR/.gitkeep b/webview-ui/src/i18n/locales/pt-BR/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/pt-br/chat.json b/webview-ui/src/i18n/locales/pt-br/chat.json deleted file mode 100644 index 9fcd1ad1e7..0000000000 --- a/webview-ui/src/i18n/locales/pt-br/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "O que o Roo pode fazer por você?" -} diff --git a/webview-ui/src/i18n/locales/pt/.gitkeep b/webview-ui/src/i18n/locales/pt/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/pt/chat.json b/webview-ui/src/i18n/locales/pt/chat.json deleted file mode 100644 index 66bef0dff9..0000000000 --- a/webview-ui/src/i18n/locales/pt/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "O que o Roo pode fazer por si?" -} diff --git a/webview-ui/src/i18n/locales/ru/.gitkeep b/webview-ui/src/i18n/locales/ru/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json deleted file mode 100644 index b01532b168..0000000000 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "Что Roo может сделать для вас?" -} diff --git a/webview-ui/src/i18n/locales/tr/.gitkeep b/webview-ui/src/i18n/locales/tr/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json deleted file mode 100644 index 8c160b2b77..0000000000 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "Roo sizin için ne yapabilir?" -} diff --git a/webview-ui/src/i18n/locales/zh-CN/.gitkeep b/webview-ui/src/i18n/locales/zh-CN/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/zh-TW/.gitkeep b/webview-ui/src/i18n/locales/zh-TW/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/webview-ui/src/i18n/locales/zh-cn/chat.json b/webview-ui/src/i18n/locales/zh-cn/chat.json deleted file mode 100644 index 4e17fd3ca0..0000000000 --- a/webview-ui/src/i18n/locales/zh-cn/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "Roo能为您做什么?" -} diff --git a/webview-ui/src/i18n/locales/zh-tw/chat.json b/webview-ui/src/i18n/locales/zh-tw/chat.json deleted file mode 100644 index 13724748a9..0000000000 --- a/webview-ui/src/i18n/locales/zh-tw/chat.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "greeting": "Roo能為您做什麼?" -}