Merge remote-tracking branch 'origin/main' into feature/add_sse_mcp

This commit is contained in:
Matt Rubens 2025-03-12 11:48:10 -04:00
commit cad274176c
78 changed files with 2805 additions and 1357 deletions

View file

@ -0,0 +1,5 @@
---
"roo-cline": patch
---
ContextProxy fix - constructor should not be async

View file

@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Add prompt caching to OpenAI-compatible custom model info

View file

@ -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

4
.gitignore vendored
View file

@ -22,7 +22,9 @@ docs/_site/
# Dotenv
.env
.env.integration
.env.*
!.env.*.sample
#Local lint config
.eslintrc.local.json

29
.roomodes Normal file
View file

@ -0,0 +1,29 @@
{
"customModes": [
{
"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",
["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"
}
]
}

View file

@ -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))
}
```

View file

@ -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": {

View file

@ -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<RooCodeAPI> | undefined
var panel: vscode.WebviewPanel | undefined
var api: RooCodeAPI
}
export async function run(): Promise<void> {
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<RooCodeAPI>("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<void>((resolve, reject) => {
try {
mocha.run((failures: number) => {
if (failures > 0) {

View file

@ -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

View file

@ -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()
})
})

View file

@ -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<void> {
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<void> {
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<void> {
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`)
}

76
e2e/src/suite/utils.ts Normal file
View file

@ -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>) | (() => boolean),
{ timeout = 30_000, interval = 250 }: WaitForOptions = {},
) => {
let timeoutId: NodeJS.Timeout | undefined = undefined
return Promise.race([
new Promise<void>((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))

View file

@ -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": {

1
package-lock.json generated
View file

@ -12546,7 +12546,6 @@
"resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz",
"integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^3.2.1",
"chalk": "^2.4.1",

View file

@ -1,4 +1,13 @@
const vscode = {
env: {
language: "en", // Default language for tests
appName: "Visual Studio Code Test",
appHost: "desktop",
appRoot: "/test/path",
machineId: "test-machine-id",
sessionId: "test-session-id",
shell: "/bin/zsh",
},
window: {
showInformationMessage: jest.fn(),
showErrorMessage: jest.fn(),

View file

@ -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<ConfigurationValues>) => {
await provider.setValues(values)
},
isReady: () => provider.viewLaunched,
getMessages: () => provider.messages,
}
}

View file

@ -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
@ -72,7 +73,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
}
if (this.options.openAiStreamingEnabled ?? true) {
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
let systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
role: "system",
content: systemPrompt,
}
@ -83,7 +84,42 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
} else if (ark) {
convertedMessages = [systemMessage, ...convertToSimpleMessages(messages)]
} else {
if (modelInfo.supportsPromptCache) {
systemMessage = {
role: "system",
content: [
{
type: "text",
text: systemPrompt,
// @ts-ignore-next-line
cache_control: { type: "ephemeral" },
},
],
}
}
convertedMessages = [systemMessage, ...convertToOpenAiMessages(messages)]
if (modelInfo.supportsPromptCache) {
// Note: the following logic is copied from openrouter:
// Add cache_control to the last two user messages
// (note: this works because we only ever add one user message at a time, but if we added multiple we'd need to mark the user message before the last assistant message)
const lastTwoUserMessages = convertedMessages.filter((msg) => msg.role === "user").slice(-2)
lastTwoUserMessages.forEach((msg) => {
if (typeof msg.content === "string") {
msg.content = [{ type: "text", text: msg.content }]
}
if (Array.isArray(msg.content)) {
// NOTE: this is fine since env details will always be added at the end. but if it weren't there, and the user added a image_url type message, it would pop a text part before it and then move it after to the end.
let lastTextPart = msg.content.filter((part) => part.type === "text").pop()
if (!lastTextPart) {
lastTextPart = { type: "text", text: "..." }
msg.content.push(lastTextPart)
}
// @ts-ignore-next-line
lastTextPart["cache_control"] = { type: "ephemeral" }
}
})
}
}
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
@ -99,15 +135,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 +165,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)

View file

@ -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() {

View file

@ -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"
@ -1102,7 +1103,6 @@ export class Cline {
browserViewportSize,
mode,
customModePrompts,
preferredLanguage,
experiments,
enableMcpServerCreation,
browserToolEnabled,
@ -1124,7 +1124,6 @@ export class Cline {
customModePrompts,
customModes,
this.customInstructions,
preferredLanguage,
this.diffEnabled,
experiments,
enableMcpServerCreation,
@ -3665,13 +3664,12 @@ export class Cline {
customModePrompts,
experiments = {} as Record<ExperimentId, boolean>,
customInstructions: globalCustomInstructions,
preferredLanguage,
} = (await this.providerRef.deref()?.getState()) ?? {}
const currentMode = mode ?? defaultModeSlug
const modeDetails = await getFullModeDetails(currentMode, customModes, customModePrompts, {
cwd,
globalCustomInstructions,
preferredLanguage,
language: formatLanguage(vscode.env.language),
})
details += `\n\n# Current Mode\n`
details += `<slug>${currentMode}</slug>\n`

View file

@ -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)
})
})
})

View file

@ -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<string, any>
private secretCache: Map<string, string | undefined>
private stateCache: Map<GlobalStateKey, any>
private secretCache: Map<SecretKey, string | undefined>
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<string | undefined>)
.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<any> | undefined {
get extension() {
return this.originalContext.extension
}
get extensionMode(): vscode.ExtensionMode {
get extensionMode() {
return this.originalContext.extensionMode
}
getGlobalState<T>(key: string): T | undefined
getGlobalState<T>(key: string, defaultValue: T): T
getGlobalState<T>(key: string, defaultValue?: T): T | undefined {
getGlobalState<T>(key: GlobalStateKey): T | undefined
getGlobalState<T>(key: GlobalStateKey, defaultValue: T): T
getGlobalState<T>(key: GlobalStateKey, defaultValue?: T): T | undefined {
const value = this.stateCache.get(key) as T | undefined
return value !== undefined ? value : (defaultValue as T | undefined)
}
updateGlobalState<T>(key: string, value: T): Thenable<void> {
updateGlobalState<T>(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<void> {
// 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<void> {
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<string, any>): Promise<void[]> {
async setValues(values: Partial<ConfigurationValues>) {
const promises: Thenable<void>[] = []
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<void> {
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()
}
}

File diff suppressed because it is too large Load diff

View file

@ -46,8 +46,6 @@ const mockContext = {
} as unknown as vscode.ExtensionContext
describe("File-Based Custom System Prompt", () => {
const experiments = {}
beforeEach(() => {
// Reset mocks before each test
jest.clearAllMocks()
@ -66,18 +64,17 @@ describe("File-Based Custom System Prompt", () => {
const prompt = await SYSTEM_PROMPT(
mockContext,
"test/path", // Using a relative path without leading slash
false,
undefined,
undefined,
undefined,
defaultModeSlug,
customModePrompts,
undefined,
undefined,
undefined,
undefined,
experiments,
true,
false, // supportsComputerUse
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
customModePrompts, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
)
// Should contain default sections
@ -101,18 +98,17 @@ describe("File-Based Custom System Prompt", () => {
const prompt = await SYSTEM_PROMPT(
mockContext,
"test/path", // Using a relative path without leading slash
false,
undefined,
undefined,
undefined,
defaultModeSlug,
undefined,
undefined,
undefined,
undefined,
undefined,
experiments,
true,
false, // supportsComputerUse
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
)
// Should contain role definition and file-based system prompt
@ -120,7 +116,6 @@ describe("File-Based Custom System Prompt", () => {
expect(prompt).toContain(fileCustomSystemPrompt)
// Should not contain any of the default sections
expect(prompt).not.toContain("TOOL USE")
expect(prompt).not.toContain("CAPABILITIES")
expect(prompt).not.toContain("MODES")
})
@ -146,18 +141,17 @@ describe("File-Based Custom System Prompt", () => {
const prompt = await SYSTEM_PROMPT(
mockContext,
"test/path", // Using a relative path without leading slash
false,
undefined,
undefined,
undefined,
defaultModeSlug,
customModePrompts,
undefined,
undefined,
undefined,
undefined,
experiments,
true,
false, // supportsComputerUse
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug, // mode
customModePrompts, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
)
// Should contain custom role definition and file-based system prompt
@ -165,7 +159,6 @@ describe("File-Based Custom System Prompt", () => {
expect(prompt).toContain(fileCustomSystemPrompt)
// Should not contain any of the default sections
expect(prompt).not.toContain("TOOL USE")
expect(prompt).not.toContain("CAPABILITIES")
expect(prompt).not.toContain("MODES")
})

View file

@ -3,20 +3,20 @@ import { getCapabilitiesSection } from "../sections/capabilities"
import { DiffStrategy, DiffResult } from "../../diff/types"
describe("addCustomInstructions", () => {
test("adds preferred language to custom instructions", async () => {
test("adds vscode language to custom instructions", async () => {
const result = await addCustomInstructions(
"mode instructions",
"global instructions",
"/test/path",
"test-mode",
{ preferredLanguage: "French" },
{ language: "fr" },
)
expect(result).toContain("Language Preference:")
expect(result).toContain("You should always speak and think in the French language")
expect(result).toContain('You should always speak and think in the "fr" language')
})
test("works without preferred language", async () => {
test("works without vscode language", async () => {
const result = await addCustomInstructions(
"mode instructions",
"global instructions",

View file

@ -6,7 +6,7 @@ import { SearchReplaceDiffStrategy } from "../../../core/diff/strategies/search-
import * as vscode from "vscode"
import fs from "fs/promises"
import os from "os"
import { defaultModeSlug, modes } from "../../../shared/modes"
import { defaultModeSlug, modes, Mode, isToolAllowedForMode } from "../../../shared/modes"
// Import path utils to get access to toPosix string extension
import "../../../utils/path"
import { addCustomInstructions } from "../sections/custom-instructions"
@ -18,46 +18,63 @@ jest.mock("../sections/modes", () => ({
getModesSection: jest.fn().mockImplementation(async () => `====\n\nMODES\n\n- Test modes section`),
}))
jest.mock("../sections/custom-instructions", () => ({
addCustomInstructions: jest
.fn()
.mockImplementation(async (modeCustomInstructions, globalCustomInstructions, cwd, mode, options) => {
const sections = []
// Mock the custom instructions
jest.mock("../sections/custom-instructions", () => {
const addCustomInstructions = jest.fn()
return {
addCustomInstructions,
__setMockImplementation: (impl: any) => {
addCustomInstructions.mockImplementation(impl)
},
}
})
// Add language preference if provided
if (options?.preferredLanguage) {
sections.push(
`Language Preference:\nYou should always speak and think in the ${options.preferredLanguage} language.`,
)
}
// Set up default mock implementation
const { __setMockImplementation } = jest.requireMock("../sections/custom-instructions")
__setMockImplementation(
async (
modeCustomInstructions: string,
globalCustomInstructions: string,
cwd: string,
mode: string,
options?: { language?: string },
) => {
const sections = []
// Add global instructions first
if (globalCustomInstructions?.trim()) {
sections.push(`Global Instructions:\n${globalCustomInstructions.trim()}`)
}
// Add language preference if provided
if (options?.language) {
sections.push(
`Language Preference:\nYou should always speak and think in the "${options.language}" language.`,
)
}
// Add mode-specific instructions after
if (modeCustomInstructions?.trim()) {
sections.push(`Mode-specific Instructions:\n${modeCustomInstructions}`)
}
// Add global instructions first
if (globalCustomInstructions?.trim()) {
sections.push(`Global Instructions:\n${globalCustomInstructions.trim()}`)
}
// Add rules
const rules = []
if (mode) {
rules.push(`# Rules from .clinerules-${mode}:\nMock mode-specific rules`)
}
rules.push(`# Rules from .clinerules:\nMock generic rules`)
// Add mode-specific instructions after
if (modeCustomInstructions?.trim()) {
sections.push(`Mode-specific Instructions:\n${modeCustomInstructions}`)
}
if (rules.length > 0) {
sections.push(`Rules:\n${rules.join("\n")}`)
}
// Add rules
const rules = []
if (mode) {
rules.push(`# Rules from .clinerules-${mode}:\nMock mode-specific rules`)
}
rules.push(`# Rules from .clinerules:\nMock generic rules`)
const joinedSections = sections.join("\n\n")
return joinedSections
? `\n====\n\nUSER'S CUSTOM INSTRUCTIONS\n\nThe following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.\n\n${joinedSections}`
: ""
}),
}))
if (rules.length > 0) {
sections.push(`Rules:\n${rules.join("\n")}`)
}
const joinedSections = sections.join("\n\n")
return joinedSections
? `\n====\n\nUSER'S CUSTOM INSTRUCTIONS\n\nThe following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.\n\n${joinedSections}`
: ""
},
)
// Mock environment-specific values for consistent tests
jest.mock("os", () => ({
@ -69,6 +86,13 @@ jest.mock("default-shell", () => "/bin/zsh")
jest.mock("os-name", () => () => "Linux")
// Mock vscode language
jest.mock("vscode", () => ({
env: {
language: "en",
},
}))
jest.mock("../../../utils/shell", () => ({
getShell: () => "/bin/zsh",
}))
@ -126,7 +150,7 @@ const createMockMcpHub = (): McpHub =>
describe("SYSTEM_PROMPT", () => {
let mockMcpHub: McpHub
let experiments: Record<string, boolean>
let experiments: Record<string, boolean> | undefined
beforeAll(() => {
// Ensure fs mock is properly initialized
@ -146,6 +170,10 @@ describe("SYSTEM_PROMPT", () => {
"/mock/mcp/path",
]
dirs.forEach((dir) => mockFs._mockDirectories.add(dir))
})
beforeEach(() => {
// Reset experiments before each test to ensure they're disabled by default
experiments = {
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: false,
[EXPERIMENT_IDS.INSERT_BLOCK]: false,
@ -175,7 +203,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -196,7 +223,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -219,7 +245,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -266,7 +291,6 @@ describe("SYSTEM_PROMPT", () => {
undefined,
undefined,
undefined,
undefined,
experiments,
true,
)
@ -286,7 +310,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -310,7 +333,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -331,7 +353,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // preferredLanguage
true, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -353,7 +374,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // preferredLanguage
false, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -375,7 +395,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -385,7 +404,11 @@ describe("SYSTEM_PROMPT", () => {
expect(prompt).toMatchSnapshot()
})
it("should include preferred language in custom instructions", async () => {
it("should include vscode language in custom instructions", async () => {
// Mock vscode.env.language
const vscode = jest.requireMock("vscode")
vscode.env = { language: "es" }
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
@ -397,14 +420,16 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
"Spanish", // preferredLanguage
undefined, // diffEnabled
experiments,
undefined, // experiments
true, // enableMcpServerCreation
)
expect(prompt).toContain("Language Preference:")
expect(prompt).toContain("You should always speak and think in the Spanish language")
expect(prompt).toContain('You should always speak and think in the "es" language')
// Reset mock
vscode.env = { language: "en" }
})
it("should include custom mode role definition at top and instructions at bottom", async () => {
@ -430,7 +455,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
customModes, // customModes
"Global instructions", // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
@ -458,18 +482,17 @@ describe("SYSTEM_PROMPT", () => {
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
false,
undefined,
undefined,
undefined,
defaultModeSlug,
customModePrompts,
undefined,
undefined,
undefined,
undefined,
experiments,
true, // enableMcpServerCreation
false, // supportsComputerUse
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug as Mode, // mode
customModePrompts, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // diffEnabled
undefined, // experiments
false, // enableMcpServerCreation
)
// Role definition from promptComponent should be at the top
@ -489,18 +512,17 @@ describe("SYSTEM_PROMPT", () => {
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
false,
undefined,
undefined,
undefined,
defaultModeSlug,
customModePrompts,
undefined,
undefined,
undefined,
undefined,
experiments,
true, // enableMcpServerCreation
false, // supportsComputerUse
undefined, // mcpHub
undefined, // diffStrategy
undefined, // browserViewportSize
defaultModeSlug as Mode, // mode
customModePrompts, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // diffEnabled
undefined, // experiments
false, // enableMcpServerCreation
)
// Should use the default mode's role definition
@ -509,6 +531,15 @@ describe("SYSTEM_PROMPT", () => {
describe("experimental tools", () => {
it("should disable experimental tools by default", async () => {
// Set experiments to explicitly disable experimental tools
const experimentsConfig = {
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: false,
[EXPERIMENT_IDS.INSERT_BLOCK]: false,
}
// Reset experiments
experiments = experimentsConfig
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
@ -520,23 +551,29 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments, // experiments - undefined should disable all experimental tools
experimentsConfig, // Explicitly disable experimental tools
true, // enableMcpServerCreation
)
// Verify experimental tools are not included in the prompt
expect(prompt).not.toContain(EXPERIMENT_IDS.SEARCH_AND_REPLACE)
expect(prompt).not.toContain(EXPERIMENT_IDS.INSERT_BLOCK)
// Check that experimental tool sections are not included
const toolSections = prompt.split("\n## ").slice(1)
const toolNames = toolSections.map((section) => section.split("\n")[0].trim())
expect(toolNames).not.toContain("search_and_replace")
expect(toolNames).not.toContain("insert_content")
expect(prompt).toMatchSnapshot()
})
it("should enable experimental tools when explicitly enabled", async () => {
const experiments = {
// Set experiments for testing experimental features
const experimentsEnabled = {
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: true,
[EXPERIMENT_IDS.INSERT_BLOCK]: true,
}
// Reset default experiments
experiments = undefined
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
@ -548,23 +585,31 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
experimentsEnabled, // Use the enabled experiments
true, // enableMcpServerCreation
)
// Get all tool sections
const toolSections = prompt.split("## ").slice(1) // Split by section headers and remove first non-tool part
const toolNames = toolSections.map((section) => section.split("\n")[0].trim())
// Verify experimental tools are included in the prompt when enabled
expect(prompt).toContain(EXPERIMENT_IDS.SEARCH_AND_REPLACE)
expect(prompt).toContain(EXPERIMENT_IDS.INSERT_BLOCK)
expect(toolNames).toContain("search_and_replace")
expect(toolNames).toContain("insert_content")
expect(prompt).toMatchSnapshot()
})
it("should selectively enable experimental tools", async () => {
const experiments = {
// Set experiments for testing selective enabling
const experimentsSelective = {
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: true,
[EXPERIMENT_IDS.INSERT_BLOCK]: false,
}
// Reset default experiments
experiments = undefined
const prompt = await SYSTEM_PROMPT(
mockContext,
"/test/path",
@ -576,15 +621,19 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
experimentsSelective, // Use the selective experiments
true, // enableMcpServerCreation
)
// Get all tool sections
const toolSections = prompt.split("## ").slice(1) // Split by section headers and remove first non-tool part
const toolNames = toolSections.map((section) => section.split("\n")[0].trim())
// Verify only enabled experimental tools are included
expect(prompt).toContain(EXPERIMENT_IDS.SEARCH_AND_REPLACE)
expect(prompt).not.toContain(EXPERIMENT_IDS.INSERT_BLOCK)
expect(toolNames).toContain("search_and_replace")
expect(toolNames).not.toContain("insert_content")
expect(prompt).toMatchSnapshot()
})
it("should list all available editing tools in base instruction", async () => {
@ -604,9 +653,8 @@ describe("SYSTEM_PROMPT", () => {
undefined,
undefined,
undefined,
undefined,
true, // diffEnabled
experiments,
experiments, // experiments
true, // enableMcpServerCreation
)
@ -616,7 +664,6 @@ describe("SYSTEM_PROMPT", () => {
expect(prompt).toContain("insert_content (for adding lines to existing files)")
expect(prompt).toContain("search_and_replace (for finding and replacing individual pieces of text)")
})
it("should provide detailed instructions for each enabled tool", async () => {
const experiments = {
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: true,
@ -634,8 +681,7 @@ describe("SYSTEM_PROMPT", () => {
undefined,
undefined,
undefined,
undefined,
true,
true, // diffEnabled
experiments,
true, // enableMcpServerCreation
)
@ -655,7 +701,7 @@ describe("SYSTEM_PROMPT", () => {
})
describe("addCustomInstructions", () => {
let experiments: Record<string, boolean>
let experiments: Record<string, boolean> | undefined
beforeAll(() => {
// Ensure fs mock is properly initialized
const mockFs = jest.requireMock("fs/promises")
@ -668,10 +714,8 @@ describe("addCustomInstructions", () => {
throw new Error(`ENOENT: no such file or directory, mkdir '${path}'`)
})
experiments = {
[EXPERIMENT_IDS.SEARCH_AND_REPLACE]: false,
[EXPERIMENT_IDS.INSERT_BLOCK]: false,
}
// Initialize experiments as undefined by default
experiments = undefined
})
beforeEach(() => {
@ -689,10 +733,9 @@ describe("addCustomInstructions", () => {
"architect", // mode
undefined, // customModePrompts
undefined, // customModes
undefined,
undefined,
undefined,
experiments,
undefined, // globalCustomInstructions
undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
)
@ -710,10 +753,9 @@ describe("addCustomInstructions", () => {
"ask", // mode
undefined, // customModePrompts
undefined, // customModes
undefined,
undefined,
undefined,
experiments,
undefined, // globalCustomInstructions
undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
)
@ -734,9 +776,8 @@ describe("addCustomInstructions", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
undefined, // experiments
true, // enableMcpServerCreation
)
@ -768,9 +809,8 @@ describe("addCustomInstructions", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
undefined, // preferredLanguage
undefined, // diffEnabled
experiments,
undefined, // experiments
false, // enableMcpServerCreation
)
@ -821,7 +861,7 @@ describe("addCustomInstructions", () => {
it("should include preferred language when provided", async () => {
const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug, {
preferredLanguage: "Spanish",
language: "es",
})
expect(instructions).toMatchSnapshot()
})
@ -837,7 +877,7 @@ describe("addCustomInstructions", () => {
"",
"/test/path",
defaultModeSlug,
{ preferredLanguage: "French" },
{ language: "fr" },
)
expect(instructions).toMatchSnapshot()
})

View file

@ -113,11 +113,11 @@ describe("addCustomInstructions", () => {
"global instructions",
"/fake/path",
"test-mode",
{ preferredLanguage: "Spanish" },
{ language: "es" },
)
expect(result).toContain("Language Preference:")
expect(result).toContain("Spanish")
expect(result).toContain("es")
expect(result).toContain("Global Instructions:\nglobal instructions")
expect(result).toContain("Mode-specific Instructions:\nmode instructions")
expect(result).toContain("Rules from .clinerules-test-mode:\nmode specific rules")

View file

@ -1,5 +1,6 @@
import fs from "fs/promises"
import path from "path"
import * as vscode from "vscode"
async function safeReadFile(filePath: string): Promise<string> {
try {
@ -33,7 +34,7 @@ export async function addCustomInstructions(
globalCustomInstructions: string,
cwd: string,
mode: string,
options: { preferredLanguage?: string; rooIgnoreInstructions?: string } = {},
options: { language?: string; rooIgnoreInstructions?: string } = {},
): Promise<string> {
const sections = []
@ -45,9 +46,9 @@ export async function addCustomInstructions(
}
// Add language preference if provided
if (options.preferredLanguage) {
if (options.language) {
sections.push(
`Language Preference:\nYou should always speak and think in the ${options.preferredLanguage} language.`,
`Language Preference:\nYou should always speak and think in the "${options.language}" language unless the user gives you instructions below to do otherwise.`,
)
}

View file

@ -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,
@ -37,7 +38,6 @@ async function generatePrompt(
promptComponent?: PromptComponent,
customModeConfigs?: ModeConfig[],
globalCustomInstructions?: string,
preferredLanguage?: string,
diffEnabled?: boolean,
experiments?: Record<string, boolean>,
enableMcpServerCreation?: boolean,
@ -90,7 +90,7 @@ ${getSystemInfoSection(cwd, mode, customModeConfigs)}
${getObjectiveSection()}
${await addCustomInstructions(promptComponent?.customInstructions || modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, { preferredLanguage, rooIgnoreInstructions })}`
${await addCustomInstructions(promptComponent?.customInstructions || modeConfig.customInstructions || "", globalCustomInstructions || "", cwd, mode, { language: formatLanguage(vscode.env.language), rooIgnoreInstructions })}`
return basePrompt
}
@ -106,7 +106,6 @@ export const SYSTEM_PROMPT = async (
customModePrompts?: CustomModePrompts,
customModes?: ModeConfig[],
globalCustomInstructions?: string,
preferredLanguage?: string,
diffEnabled?: boolean,
experiments?: Record<string, boolean>,
enableMcpServerCreation?: boolean,
@ -135,11 +134,19 @@ export const SYSTEM_PROMPT = async (
// If a file-based custom system prompt exists, use it
if (fileCustomSystemPrompt) {
const roleDefinition = promptComponent?.roleDefinition || currentMode.roleDefinition
const customInstructions = await addCustomInstructions(
promptComponent?.customInstructions || currentMode.customInstructions || "",
globalCustomInstructions || "",
cwd,
mode,
{ language: formatLanguage(vscode.env.language), rooIgnoreInstructions },
)
// For file-based prompts, don't include the tool sections
return `${roleDefinition}
${fileCustomSystemPrompt}
${await addCustomInstructions(promptComponent?.customInstructions || currentMode.customInstructions || "", globalCustomInstructions || "", cwd, mode, { preferredLanguage, rooIgnoreInstructions })}`
${customInstructions}`
}
// If diff is disabled, don't pass the diffStrategy
@ -156,7 +163,6 @@ ${await addCustomInstructions(promptComponent?.customInstructions || currentMode
promptComponent,
customModes,
globalCustomInstructions,
preferredLanguage,
diffEnabled,
experiments,
enableMcpServerCreation,

View file

@ -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,13 +13,20 @@ 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"
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"
@ -387,6 +393,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
@ -1379,10 +1390,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("rateLimitSeconds", message.value ?? 0)
await this.postStateToWebview()
break
case "preferredLanguage":
await this.updateGlobalState("preferredLanguage", message.text)
await this.postStateToWebview()
break
case "writeDelayMs":
await this.updateGlobalState("writeDelayMs", message.value)
await this.postStateToWebview()
@ -1920,7 +1927,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customModePrompts,
customInstructions,
preferredLanguage,
browserViewportSize,
diffEnabled,
mcpEnabled,
@ -1958,7 +1964,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
customModePrompts,
customModes,
customInstructions,
preferredLanguage,
diffEnabled,
experiments,
enableMcpServerCreation,
@ -2013,18 +2018,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()) {
@ -2333,7 +2339,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
screenshotQuality,
remoteBrowserHost,
remoteBrowserEnabled,
preferredLanguage,
writeDelayMs,
terminalOutputLineLimit,
fuzzyMatchThreshold,
@ -2354,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
@ -2393,7 +2399,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
screenshotQuality: screenshotQuality ?? 75,
remoteBrowserHost,
remoteBrowserEnabled: remoteBrowserEnabled ?? false,
preferredLanguage: preferredLanguage ?? "English",
writeDelayMs: writeDelayMs ?? 1000,
terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
@ -2419,6 +2424,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
telemetryKey,
machineId,
showRooIgnoredFiles: showRooIgnoredFiles ?? true,
language,
}
}
@ -2552,37 +2558,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
writeDelayMs: stateValues.writeDelayMs ?? 1000,
terminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500,
mode: stateValues.mode ?? defaultModeSlug,
preferredLanguage:
stateValues.preferredLanguage ??
(() => {
// Get VSCode's locale setting
const vscodeLang = vscode.env.language
// Map VSCode locale to our supported languages
const langMap: { [key: string]: string } = {
en: "English",
ar: "Arabic",
"pt-br": "Brazilian Portuguese",
ca: "Catalan",
cs: "Czech",
fr: "French",
de: "German",
hi: "Hindi",
hu: "Hungarian",
it: "Italian",
ja: "Japanese",
ko: "Korean",
pl: "Polish",
pt: "Portuguese",
ru: "Russian",
zh: "Simplified Chinese",
"zh-cn": "Simplified Chinese",
es: "Spanish",
"zh-tw": "Traditional Chinese",
tr: "Turkish",
}
// Return mapped language or default to English
return langMap[vscodeLang] ?? langMap[vscodeLang.split("-")[0]] ?? "English"
})(),
// Pass the VSCode language code directly
language: formatLanguage(vscode.env.language),
mcpEnabled: stateValues.mcpEnabled ?? true,
enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true,
alwaysApproveResubmit: stateValues.alwaysApproveResubmit ?? false,
@ -2620,11 +2597,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 +2615,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
return await this.contextProxy.getSecret(key)
}
// global + secret
public async setValues(values: Partial<ConfigurationValues>) {
await this.contextProxy.setValues(values)
}
// dev
async resetState() {

View file

@ -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,
@ -418,7 +420,6 @@ describe("ClineProvider", () => {
const mockState: ExtensionState = {
version: "1.0.0",
preferredLanguage: "English",
clineMessages: [],
taskHistory: [],
shouldShowAnnouncement: false,
@ -533,20 +534,12 @@ describe("ClineProvider", () => {
expect(state).toHaveProperty("writeDelayMs")
})
test("preferredLanguage defaults to VSCode language when not set", 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.preferredLanguage).toBe("Spanish")
})
test("preferredLanguage defaults to English for unsupported VSCode language", async () => {
// Mock VSCode language as an unsupported language
;(vscode.env as any).language = "unsupported-LANG"
const state = await provider.getState()
expect(state.preferredLanguage).toBe("English")
expect(state.language).toBe("es-ES")
})
test("diffEnabled defaults to true when not set", async () => {
@ -1214,7 +1207,7 @@ describe("ClineProvider", () => {
expect(callArgs[4]).toHaveProperty("getToolDescription") // diffStrategy
expect(callArgs[5]).toBe("900x600") // browserViewportSize
expect(callArgs[6]).toBe("code") // mode
expect(callArgs[11]).toBe(true) // diffEnabled
expect(callArgs[10]).toBe(true) // diffEnabled
// Run the test again to verify it's consistent
await handler({ type: "getSystemPrompt", mode: "code" })
@ -1272,7 +1265,7 @@ describe("ClineProvider", () => {
expect(callArgs[4]).toHaveProperty("getToolDescription") // diffStrategy
expect(callArgs[5]).toBe("900x600") // browserViewportSize
expect(callArgs[6]).toBe("code") // mode
expect(callArgs[11]).toBe(false) // diffEnabled should be false
expect(callArgs[10]).toBe(false) // diffEnabled should be false
})
test("uses correct mode-specific instructions when mode is specified", async () => {

View file

@ -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...")

View file

@ -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<void>
/**
* Retrieves the custom instructions from the global storage.
* @returns The saved custom instructions, or undefined if not set.
*/
getCustomInstructions(): Promise<string | undefined>
/**
* 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<void>
/**
* Cancels the current task.
*/
cancelTask(): Promise<void>
/**
* Sends a message to the current task.
* @param message Optional message to send.
@ -36,9 +29,20 @@ export interface RooCodeAPI {
pressSecondaryButton(): Promise<void>
/**
* 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<ConfigurationValues>): Promise<void>
/**
* 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,105 @@ 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<void>
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"
| "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<void>
export type ConfigurationKey = GlobalStateKey | SecretKey
/**
* Initializes Cline with a history item
*/
initClineWithHistoryItem(historyItem: HistoryItem): Promise<void>
/**
* Posts a message to the webview
*/
postMessageToWebview(message: ExtensionMessage): Promise<void>
/**
* Handles mode switching
*/
handleModeSwitch(newMode: Mode): Promise<void>
/**
* Updates custom instructions
*/
updateCustomInstructions(instructions?: string): Promise<void>
/**
* Cancels the current task
*/
cancelTask(): Promise<void>
/**
* Gets the current state
*/
getState(): Promise<any>
/**
* 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<void>
/**
* Gets a value from the global state
* @param key The key to get
*/
getGlobalState(key: GlobalStateKey): Promise<any>
/**
* 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<void>
/**
* Resets the state
*/
resetState(): Promise<void>
/**
* Logs a message
*/
log(message: string): void
/**
* Disposes of the provider
*/
dispose(): Promise<void>
}
export type ConfigurationValues = Record<ConfigurationKey, any>

View file

@ -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
@ -131,7 +131,7 @@ export interface ExtensionState {
remoteBrowserHost?: string
remoteBrowserEnabled?: boolean
fuzzyMatchThreshold?: number
preferredLanguage: string
language?: string
writeDelayMs: number
terminalOutputLineLimit?: number
mcpEnabled: boolean

View file

@ -64,7 +64,6 @@ export interface WebviewMessage {
| "toggleMcpServer"
| "updateMcpTimeout"
| "fuzzyMatchThreshold"
| "preferredLanguage"
| "writeDelayMs"
| "enhancePrompt"
| "enhancedPrompt"

View file

@ -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")
})
})

View file

@ -6,7 +6,6 @@ jest.mock("../../core/prompts/sections/custom-instructions", () => ({
}))
import { isToolAllowedForMode, FileRestrictionError, ModeConfig, getFullModeDetails, modes } from "../modes"
import * as vscode from "vscode"
import { addCustomInstructions } from "../../core/prompts/sections/custom-instructions"
describe("isToolAllowedForMode", () => {
@ -402,7 +401,7 @@ describe("FileRestrictionError", () => {
const options = {
cwd: "/test/path",
globalCustomInstructions: "Global instructions",
preferredLanguage: "en",
language: "en",
}
await getFullModeDetails("debug", undefined, undefined, options)
@ -412,7 +411,7 @@ describe("FileRestrictionError", () => {
"Global instructions",
"/test/path",
"debug",
{ preferredLanguage: "en" },
{ language: "en" },
)
})

View file

@ -342,6 +342,14 @@ export const bedrockModels = {
inputPrice: 0.25,
outputPrice: 1.25,
},
"deepseek.r1-v1:0": {
maxTokens: 32_768,
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 1.35,
outputPrice: 5.4,
},
"meta.llama3-3-70b-instruct-v1:0": {
maxTokens: 8192,
contextWindow: 128_000,

View file

@ -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 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
* 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<SecretKey, (typeof SECRET_KEYS)[number]> 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,6 @@ export const GLOBAL_STATE_KEYS = [
"screenshotQuality",
"remoteBrowserHost",
"fuzzyMatchThreshold",
"preferredLanguage", // Language setting for Cline's communication
"writeDelayMs",
"terminalOutputLineLimit",
"mcpEnabled",
@ -85,10 +97,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 +117,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<GlobalStateKey, (typeof GLOBAL_STATE_KEYS)[number]> 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)

14
src/shared/language.ts Normal file
View file

@ -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()}`)
}

View file

@ -195,10 +195,13 @@ export function isToolAllowedForMode(
}
// Check tool requirements if any exist
if (toolRequirements && tool in toolRequirements) {
if (!toolRequirements[tool]) {
if (toolRequirements && typeof toolRequirements === "object") {
if (tool in toolRequirements && !toolRequirements[tool]) {
return false
}
} else if (toolRequirements === false) {
// If toolRequirements is a boolean false, all tools are disabled
return false
}
const mode = getModeBySlug(modeSlug, customModes)
@ -275,7 +278,7 @@ export async function getFullModeDetails(
options?: {
cwd?: string
globalCustomInstructions?: string
preferredLanguage?: string
language?: string
},
): Promise<ModeConfig> {
// First get the base mode config from custom modes or built-in modes
@ -295,7 +298,7 @@ export async function getFullModeDetails(
options.globalCustomInstructions || "",
options.cwd,
modeSlug,
{ preferredLanguage: options.preferredLanguage },
{ language: options.language },
)
}

View file

@ -23,3 +23,5 @@ yarn-debug.log*
yarn-error.log*
*storybook.log
tsconfig.tsbuildinfo

View file

@ -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: ["<rootDir>/src/**/__tests__/**/*.{js,jsx,ts,tsx}", "<rootDir>/src/**/*.{spec,test}.{js,jsx,ts,tsx}"],
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
moduleNameMapper: {
@ -12,6 +12,12 @@ module.exports = {
"^vscrui$": "<rootDir>/src/__mocks__/vscrui.ts",
"^@vscode/webview-ui-toolkit/react$": "<rootDir>/src/__mocks__/@vscode/webview-ui-toolkit/react.ts",
"^@/(.*)$": "<rootDir>/src/$1",
"^src/i18n/setup$": "<rootDir>/src/__mocks__/i18n/setup.ts",
"^\\.\\./setup$": "<rootDir>/src/__mocks__/i18n/setup.ts",
"^\\./setup$": "<rootDir>/src/__mocks__/i18n/setup.ts",
"^src/i18n/TranslationContext$": "<rootDir>/src/__mocks__/i18n/TranslationContext.tsx",
"^\\.\\./TranslationContext$": "<rootDir>/src/__mocks__/i18n/TranslationContext.tsx",
"^\\./TranslationContext$": "<rootDir>/src/__mocks__/i18n/TranslationContext.tsx"
},
reporters: [["jest-simple-dot-reporter", {}]],
transformIgnorePatterns: [

View file

@ -28,11 +28,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 +81,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"
}
},
@ -6165,26 +6168,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 +6286,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",
@ -6645,7 +6538,6 @@
"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",
@ -6666,7 +6558,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 +6599,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"
},
@ -6760,7 +6650,6 @@
"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
},
"node_modules/@types/babel__core": {
@ -7771,116 +7660,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",
@ -8239,18 +8018,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,25 +8537,6 @@
"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",
@ -8856,18 +8604,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",
@ -9568,6 +9304,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 +10021,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",
@ -10461,7 +10193,6 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/domexception": {
@ -13070,6 +12801,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 +12875,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 +15379,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",
@ -15650,7 +15418,6 @@
"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 +17560,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 +18057,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",
@ -18725,6 +18518,27 @@
"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",
@ -20783,30 +20597,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 +20931,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 +21531,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",

View file

@ -35,11 +35,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 +88,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"
}
}

View file

@ -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 = () => (
<ExtensionStateContextProvider>
<App />
<TranslationProvider>
<App />
</TranslationProvider>
</ExtensionStateContextProvider>
)

View file

@ -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, any>) => string
i18n: typeof i18next
}>({
t: (key: string, options?: Record<string, any>) => {
// 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 (
<TranslationContext.Provider
value={{
t: (key: string, options?: Record<string, any>) => {
// 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}
</TranslationContext.Provider>
)
}
// Custom hook for easy translations
export const useAppTranslation = () => React.useContext(TranslationContext)
export default TranslationProvider

View file

@ -0,0 +1,62 @@
import i18next from "i18next"
import { initReactI18next } from "react-i18next"
// Mock translations for testing
const translations: Record<string, Record<string, any>> = {
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

View file

@ -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" && <TelemetryBanner />}
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
<div style={{ padding: "0 20px", flexShrink: 0 }}>
<h2>What can Roo do for you?</h2>
<h2>{t("chat:greeting")}</h2>
<p>
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

View file

@ -9,19 +9,6 @@ import {
VSCodeRadioGroup,
VSCodeRadio,
} from "@vscode/webview-ui-toolkit/react"
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons"
import {
Button,
Command,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui"
import { cn } from "@/lib/utils"
import { useExtensionState } from "../../context/ExtensionStateContext"
import {
Mode,
@ -43,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)
@ -69,8 +57,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
mode,
customInstructions,
setCustomInstructions,
preferredLanguage,
setPreferredLanguage,
customModes,
enableCustomModeCreation,
setEnableCustomModeCreation,
@ -82,9 +68,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
const [testPrompt, setTestPrompt] = useState("")
const [isEnhancing, setIsEnhancing] = useState(false)
const [isDialogOpen, setIsDialogOpen] = useState(false)
const [open, setOpen] = useState(false)
const [isCustomLanguage, setIsCustomLanguage] = useState(false)
const [customLanguage, setCustomLanguage] = useState("")
const [selectedPromptContent, setSelectedPromptContent] = useState("")
const [selectedPromptTitle, setSelectedPromptTitle] = useState("")
const [isToolsEditMode, setIsToolsEditMode] = useState(false)
@ -427,136 +410,7 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</TabHeader>
<TabContent>
<div className="pb-5 border-b border-vscode-input-border">
<div className="mb-5">
<div className="font-bold mb-1">Preferred Language</div>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="combobox"
role="combobox"
aria-expanded={open}
className="w-full justify-between">
{preferredLanguage ?? "Select language..."}
<CaretSortIcon className="opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent align="start" className="p-0">
<Command>
<CommandInput placeholder="Search language..." className="h-9" />
<CommandList>
<CommandGroup>
{[
{ 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) => (
<CommandItem
key={language.value}
value={language.value}
onSelect={(value) => {
setPreferredLanguage(value)
vscode.postMessage({
type: "preferredLanguage",
text: value,
})
setOpen(false)
}}>
{language.label}
<CheckIcon
className={cn(
"ml-auto",
preferredLanguage === language.value
? "opacity-100"
: "opacity-0",
)}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
<div className="border-t border-[var(--vscode-input-border)]">
<button
className="w-full px-2 py-1.5 text-sm text-left hover:bg-[var(--vscode-list-hoverBackground)]"
onClick={() => {
setIsCustomLanguage(true)
setOpen(false)
}}>
+ Choose another language
</button>
</div>
</PopoverContent>
</Popover>
<p className="text-xs mt-1.5 text-vscode-descriptionForeground">
Select the language that Roo should use for communication.
</p>
</div>
<div className="font-bold mb-1">Custom Instructions for All Modes</div>
<div className="text-sm text-vscode-descriptionForeground mb-2">
These instructions apply to all modes. They provide a base set of behaviors that can be enhanced
by mode-specific instructions below.
</div>
<VSCodeTextArea
value={customInstructions ?? ""}
onChange={(e) => {
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"
/>
<div className="text-xs text-vscode-descriptionForeground mt-1.5 mb-10">
Instructions can also be loaded from{" "}
<span
className="text-vscode-textLink-foreground cursor-pointer underline"
onClick={() =>
vscode.postMessage({
type: "openFile",
text: "./.clinerules",
values: {
create: true,
content: "",
},
})
}>
.clinerules
</span>{" "}
in your workspace.
</div>
</div>
<div className="mt-5">
<div>
<div onClick={(e) => e.stopPropagation()} className="flex justify-between items-center mb-3">
<h3 className="text-vscode-foreground m-0">Modes</h3>
<div className="flex gap-2">
@ -987,8 +841,37 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</VSCodeButton>
</div>
{/*
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.
*/}
<div className="mt-12">
<VSCodeCheckbox
checked={enableCustomModeCreation ?? true}
onChange={(e: any) => {
// Just update the local state through React context
// The React context will update the global state
setEnableCustomModeCreation(e.target.checked)
}}>
<span style={{ fontWeight: "500" }}>Enable Custom Mode Creation Through Prompts</span>
</VSCodeCheckbox>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
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
isnt needed. When disabled you can still manually create custom modes using the + button
above or by editing the related config JSON.
</p>
</div>
{/* Custom System Prompt Disclosure */}
<div className="mb-3 mt-12">
<div className="mt-12">
<button
onClick={() => setIsSystemPromptDisclosureOpen(!isSystemPromptDisclosureOpen)}
className="flex items-center text-xs text-vscode-foreground hover:text-vscode-textLink-foreground focus:outline-none"
@ -1025,34 +908,54 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
)}
</div>
</div>
{/*
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.
*/}
<div className="mb-4 mt-4">
<VSCodeCheckbox
checked={enableCustomModeCreation ?? true}
onChange={(e: any) => {
// Just update the local state through React context
// The React context will update the global state
setEnableCustomModeCreation(e.target.checked)
}}>
<span style={{ fontWeight: "500" }}>Enable Custom Mode Creation Through Prompts</span>
</VSCodeCheckbox>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
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
isnt needed. When disabled you can still manually create custom modes using the + button
above or by editing the related config JSON.
</p>
<div className="pb-5 border-b border-vscode-input-border">
<h3 style={{ color: "var(--vscode-foreground)", marginBottom: "12px" }}>
Custom Instructions for All Modes
</h3>
<div className="text-sm text-vscode-descriptionForeground mb-2">
These instructions apply to all modes. They provide a base set of behaviors that can be enhanced
by mode-specific instructions below.
<br />
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.
</div>
<VSCodeTextArea
value={customInstructions ?? ""}
onChange={(e) => {
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"
/>
<div className="text-xs text-vscode-descriptionForeground mt-1.5 mb-10">
Instructions can also be loaded from{" "}
<span
className="text-vscode-textLink-foreground cursor-pointer underline"
onClick={() =>
vscode.postMessage({
type: "openFile",
text: "./.clinerules",
values: {
create: true,
content: "",
},
})
}>
.clinerules
</span>{" "}
in your workspace.
</div>
</div>
@ -1509,41 +1412,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
</div>
</div>
)}
{isCustomLanguage && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-[var(--vscode-editor-background)] p-6 rounded-lg w-96">
<h3 className="text-lg font-semibold mb-4">Add Custom Language</h3>
<input
type="text"
className="w-full p-2 mb-4 bg-[var(--vscode-input-background)] text-[var(--vscode-input-foreground)] border border-[var(--vscode-input-border)] rounded"
placeholder="Enter language name"
value={customLanguage}
onChange={(e) => setCustomLanguage(e.target.value)}
/>
<div className="flex justify-end gap-2">
<Button variant="secondary" onClick={() => setIsCustomLanguage(false)}>
Cancel
</Button>
<Button
onClick={() => {
if (customLanguage.trim()) {
setPreferredLanguage(customLanguage.trim())
vscode.postMessage({
type: "preferredLanguage",
text: customLanguage.trim(),
})
setIsCustomLanguage(false)
setCustomLanguage("")
}
}}
disabled={!customLanguage.trim()}>
Add
</Button>
</div>
</div>
</div>
)}
</Tab>
)
}

View file

@ -819,7 +819,7 @@ const ApiOptions = ({
style={{ fontSize: "12px" }}
/>
</div>
<div className="text-sm text-vscode-descriptionForeground">
<div className="text-sm text-vscode-descriptionForeground pt-1">
Is this model capable of processing and understanding images?
</div>
</div>
@ -842,11 +842,34 @@ const ApiOptions = ({
style={{ fontSize: "12px" }}
/>
</div>
<div className="text-sm text-vscode-descriptionForeground [pt">
<div className="text-sm text-vscode-descriptionForeground pt-1">
Is this model capable of interacting with a browser? (e.g. Claude 3.7 Sonnet).
</div>
</div>
<div>
<div className="flex items-center gap-1">
<Checkbox
checked={apiConfiguration?.openAiCustomModelInfo?.supportsPromptCache ?? false}
onChange={handleInputChange("openAiCustomModelInfo", (checked) => {
return {
...(apiConfiguration?.openAiCustomModelInfo || openAiModelInfoSaneDefaults),
supportsPromptCache: checked,
}
})}>
<span className="font-medium">Prompt Caching</span>
</Checkbox>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title="Enable if the model supports prompt caching. This can improve performance and reduce costs."
style={{ fontSize: "12px" }}
/>
</div>
<div className="text-sm text-vscode-descriptionForeground pt-1">
Is this model capable of caching prompts?
</div>
</div>
<div>
<VSCodeTextField
value={
@ -933,6 +956,93 @@ const ApiOptions = ({
</VSCodeTextField>
</div>
{apiConfiguration?.openAiCustomModelInfo?.supportsPromptCache && (
<>
<div>
<VSCodeTextField
value={
apiConfiguration?.openAiCustomModelInfo?.cacheReadsPrice?.toString() ?? "0"
}
type="text"
style={{
borderColor: (() => {
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">
<div className="flex items-center gap-1">
<span className="font-medium">Cache Reads Price</span>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title="Cost per million tokens for reading from the cache. This is the price charged when a cached response is retrieved."
style={{ fontSize: "12px" }}
/>
</div>
</VSCodeTextField>
</div>
<div>
<VSCodeTextField
value={
apiConfiguration?.openAiCustomModelInfo?.cacheWritesPrice?.toString() ?? "0"
}
type="text"
style={{
borderColor: (() => {
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">
<div className="flex items-center gap-1">
<span className="font-medium">Cache Writes Price</span>
<i
className="codicon codicon-info text-vscode-descriptionForeground"
title="Cost per million tokens for writing to the cache. This is the price charged when a prompt is cached for the first time."
style={{ fontSize: "12px" }}
/>
</div>
</VSCodeTextField>
</div>
</>
)}
<Button
variant="secondary"
onClick={() =>

View file

@ -50,7 +50,7 @@ export type ComboboxType = "single" | "multiple"
export interface ComboboxBaseProps
extends React.ComponentProps<typeof PopoverPrimitive.Root>,
Omit<React.ComponentProps<typeof CommandPrimitive>, "value" | "defaultValue" | "onValueChange"> {
Omit<React.ComponentProps<typeof CommandPrimitive>, "value" | "defaultValue" | "onValueChange" | "children"> {
type?: ComboboxType | undefined
inputValue?: string
defaultInputValue?: string

View file

@ -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",
language: "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) =>

View file

@ -109,7 +109,6 @@ describe("mergeExtensionState", () => {
shouldShowAnnouncement: false,
enableCheckpoints: true,
checkpointStorage: "task",
preferredLanguage: "English",
writeDelayMs: 1000,
requestDelaySeconds: 5,
rateLimitSeconds: 0,

View file

@ -0,0 +1,57 @@
import React, { createContext, useContext, ReactNode, useEffect, useCallback } 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, any>) => 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])
// Memoize the translation function to prevent unnecessary re-renders
const translate = useCallback(
(key: string, options?: Record<string, any>) => {
return i18n.t(key, options)
},
[i18n],
)
return (
<TranslationContext.Provider
value={{
t: translate,
i18n,
}}>
{children}
</TranslationContext.Provider>
)
}
// Custom hook for easy translations
export const useAppTranslation = () => useContext(TranslationContext)
export default TranslationProvider

View file

@ -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 (
<div>
<h1 data-testid="translation-test">{t("settings.autoApprove.title")}</h1>
<p data-testid="translation-interpolation">{t("notifications.error", { message: "Test error" })}</p>
</div>
)
}
describe("TranslationContext", () => {
beforeAll(() => {
// Initialize i18next with test translations
setupI18nForTests()
})
it("should provide translations via context", () => {
const { getByTestId } = render(
<TranslationProvider>
<TestComponent />
</TranslationProvider>,
)
// Check if translation is provided correctly
expect(getByTestId("translation-test")).toHaveTextContent("Auto-Approve")
})
it("should handle interpolation correctly", () => {
const { getByTestId } = render(
<TranslationProvider>
<TestComponent />
</TranslationProvider>,
)
// Check if interpolation works
expect(getByTestId("translation-interpolation")).toHaveTextContent("Operation failed: Test error")
})
})

View file

View file

View file

View file

View file

View file

@ -0,0 +1,3 @@
{
"greeting": "What can Roo do for you?"
}

View file

View file

View file

View file

View file

View file

View file

View file

View file

View file

View file

View file

@ -0,0 +1,54 @@
import i18next from "i18next"
import { initReactI18next } from "react-i18next"
// Build translations object
const translations: Record<string, Record<string, any>> = {}
// 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

View file

@ -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
}