mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-09 22:31:08 +00:00
Merge remote-tracking branch 'origin/main' into feature/add_sse_mcp
This commit is contained in:
commit
cad274176c
78 changed files with 2805 additions and 1357 deletions
5
.changeset/seven-apricots-watch.md
Normal file
5
.changeset/seven-apricots-watch.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
ContextProxy fix - constructor should not be async
|
||||
5
.changeset/thin-fans-deliver.md
Normal file
5
.changeset/thin-fans-deliver.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"roo-cline": patch
|
||||
---
|
||||
|
||||
Add prompt caching to OpenAI-compatible custom model info
|
||||
4
.github/workflows/code-qa.yml
vendored
4
.github/workflows/code-qa.yml
vendored
|
|
@ -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
4
.gitignore
vendored
|
|
@ -22,7 +22,9 @@ docs/_site/
|
|||
|
||||
# Dotenv
|
||||
.env
|
||||
.env.integration
|
||||
.env.*
|
||||
!.env.*.sample
|
||||
|
||||
|
||||
#Local lint config
|
||||
.eslintrc.local.json
|
||||
|
|
|
|||
29
.roomodes
Normal file
29
.roomodes
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
58
e2e/src/suite/subtasks.test.ts
Normal file
58
e2e/src/suite/subtasks.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
|
|
@ -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
76
e2e/src/suite/utils.ts
Normal 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))
|
||||
|
|
@ -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
1
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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")
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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.`,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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...")
|
||||
|
||||
|
|
|
|||
209
src/exports/roo-code.d.ts
vendored
209
src/exports/roo-code.d.ts
vendored
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@ export interface WebviewMessage {
|
|||
| "toggleMcpServer"
|
||||
| "updateMcpTimeout"
|
||||
| "fuzzyMatchThreshold"
|
||||
| "preferredLanguage"
|
||||
| "writeDelayMs"
|
||||
| "enhancePrompt"
|
||||
| "enhancedPrompt"
|
||||
|
|
|
|||
19
src/shared/__tests__/language.test.ts
Normal file
19
src/shared/__tests__/language.test.ts
Normal 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")
|
||||
})
|
||||
})
|
||||
|
|
@ -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" },
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
14
src/shared/language.ts
Normal 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()}`)
|
||||
}
|
||||
|
|
@ -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 },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
2
webview-ui/.gitignore
vendored
2
webview-ui/.gitignore
vendored
|
|
@ -23,3 +23,5 @@ yarn-debug.log*
|
|||
yarn-error.log*
|
||||
|
||||
*storybook.log
|
||||
|
||||
tsconfig.tsbuildinfo
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
463
webview-ui/package-lock.json
generated
463
webview-ui/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
||||
|
|
|
|||
47
webview-ui/src/__mocks__/i18n/TranslationContext.tsx
Normal file
47
webview-ui/src/__mocks__/i18n/TranslationContext.tsx
Normal 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
|
||||
62
webview-ui/src/__mocks__/i18n/setup.ts
Normal file
62
webview-ui/src/__mocks__/i18n/setup.ts
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
isn’t 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
|
||||
isn’t 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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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={() =>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -109,7 +109,6 @@ describe("mergeExtensionState", () => {
|
|||
shouldShowAnnouncement: false,
|
||||
enableCheckpoints: true,
|
||||
checkpointStorage: "task",
|
||||
preferredLanguage: "English",
|
||||
writeDelayMs: 1000,
|
||||
requestDelaySeconds: 5,
|
||||
rateLimitSeconds: 0,
|
||||
|
|
|
|||
57
webview-ui/src/i18n/TranslationContext.tsx
Normal file
57
webview-ui/src/i18n/TranslationContext.tsx
Normal 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
|
||||
52
webview-ui/src/i18n/__tests__/TranslationContext.test.tsx
Normal file
52
webview-ui/src/i18n/__tests__/TranslationContext.test.tsx
Normal 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")
|
||||
})
|
||||
})
|
||||
0
webview-ui/src/i18n/locales/ar/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/ar/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/ca/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/ca/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/cs/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/cs/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/de/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/de/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/en/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/en/.gitkeep
Normal file
3
webview-ui/src/i18n/locales/en/chat.json
Normal file
3
webview-ui/src/i18n/locales/en/chat.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"greeting": "What can Roo do for you?"
|
||||
}
|
||||
0
webview-ui/src/i18n/locales/es/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/es/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/fr/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/fr/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/hi/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/hi/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/hu/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/hu/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/it/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/it/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/ja/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/ja/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/ko/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/ko/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/pl/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/pl/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/pt-BR/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/pt-BR/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/pt/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/pt/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/ru/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/ru/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/tr/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/tr/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/zh-CN/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/zh-CN/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/zh-TW/.gitkeep
Normal file
0
webview-ui/src/i18n/locales/zh-TW/.gitkeep
Normal file
54
webview-ui/src/i18n/setup.ts
Normal file
54
webview-ui/src/i18n/setup.ts
Normal 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
|
||||
37
webview-ui/src/i18n/test-utils.ts
Normal file
37
webview-ui/src/i18n/test-utils.ts
Normal 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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue