diff --git a/jest.config.js b/jest.config.js index cd4944c5477..8bbecf9af06 100644 --- a/jest.config.js +++ b/jest.config.js @@ -36,6 +36,7 @@ module.exports = { "^default-shell$": "/src/__mocks__/default-shell.js", "^os-name$": "/src/__mocks__/os-name.js", "^strip-bom$": "/src/__mocks__/strip-bom.js", + "^execa$": "/src/__mocks__/execa.js", "^@roo/(.*)$": "/src/$1", "^@src/(.*)$": "/webview-ui/src/$1", }, diff --git a/package-lock.json b/package-lock.json index 5d7799e0150..9ab2b6f9e7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40,7 +40,7 @@ "monaco-vscode-textmate-theme-converter": "^0.1.7", "node-cache": "^5.1.2", "node-ipc": "^12.0.0", - "openai": "^4.78.1", + "openai": "^6.42.0", "os-name": "^6.0.0", "p-wait-for": "^5.0.2", "pdf-parse": "^1.1.1", @@ -66,7 +66,7 @@ "vscode-material-icons": "^0.1.1", "web-tree-sitter": "^0.22.6", "workerpool": "^9.2.0", - "zod": "^3.23.8" + "zod": "^3.25.76" }, "devDependencies": { "@changesets/cli": "^2.27.10", @@ -5904,15 +5904,6 @@ "node": ">=18.0.0" } }, - "node_modules/@google/genai/node_modules/zod": { - "version": "3.24.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", - "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/@google/genai/node_modules/zod-to-json-schema": { "version": "3.24.5", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", @@ -6762,15 +6753,6 @@ "node": ">=18" } }, - "node_modules/@modelcontextprotocol/sdk/node_modules/zod": { - "version": "3.24.2", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", - "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/@modelcontextprotocol/sdk/node_modules/zod-to-json-schema": { "version": "3.24.3", "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.3.tgz", @@ -10876,6 +10858,15 @@ "devtools-protocol": "*" } }, + "node_modules/chromium-bidi/node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -17502,43 +17493,23 @@ } }, "node_modules/openai": { - "version": "4.78.1", - "resolved": "https://registry.npmjs.org/openai/-/openai-4.78.1.tgz", - "integrity": "sha512-drt0lHZBd2lMyORckOXFPQTmnGLWSLt8VK0W9BhOKWpMFBEoHMoz5gxMPmVq5icp+sOrsbMnsmZTVHUlKvD1Ow==", - "dependencies": { - "@types/node": "^18.11.18", - "@types/node-fetch": "^2.6.4", - "abort-controller": "^3.0.0", - "agentkeepalive": "^4.2.1", - "form-data-encoder": "1.7.2", - "formdata-node": "^4.3.2", - "node-fetch": "^2.6.7" - }, - "bin": { - "openai": "bin/cli" - }, + "version": "6.42.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.42.0.tgz", + "integrity": "sha512-1WFEt/uXMXOLhYRNkgJWo08Y2YNvNwpVU72K7ibrWgWpNOXd4VojXLbe6SQ4bLiUQ3Y8jz4IiyVkylJCL1DtZg==", + "license": "Apache-2.0", "peerDependencies": { - "zod": "^3.23.8" + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" }, "peerDependenciesMeta": { + "ws": { + "optional": true + }, "zod": { "optional": true } } }, - "node_modules/openai/node_modules/@types/node": { - "version": "18.19.67", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.67.tgz", - "integrity": "sha512-wI8uHusga+0ZugNp0Ol/3BqQfEcCCNfojtO6Oou9iVNGPTL6QNSdnUdqq85fRgIorLhLMuPIKpsN98QE9Nh+KQ==", - "dependencies": { - "undici-types": "~5.26.4" - } - }, - "node_modules/openai/node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" - }, "node_modules/option": { "version": "0.2.4", "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", @@ -22571,9 +22542,10 @@ } }, "node_modules/zod": { - "version": "3.23.8", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", - "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 1b372e92267..a3ba5133e42 100644 --- a/package.json +++ b/package.json @@ -409,7 +409,7 @@ "monaco-vscode-textmate-theme-converter": "^0.1.7", "node-cache": "^5.1.2", "node-ipc": "^12.0.0", - "openai": "^4.78.1", + "openai": "^6.42.0", "os-name": "^6.0.0", "p-wait-for": "^5.0.2", "pdf-parse": "^1.1.1", @@ -435,7 +435,7 @@ "vscode-material-icons": "^0.1.1", "web-tree-sitter": "^0.22.6", "workerpool": "^9.2.0", - "zod": "^3.23.8" + "zod": "^3.25.76" }, "devDependencies": { "@changesets/cli": "^2.27.10", diff --git a/src/__mocks__/execa.js b/src/__mocks__/execa.js new file mode 100644 index 00000000000..82db81dd0b6 --- /dev/null +++ b/src/__mocks__/execa.js @@ -0,0 +1,38 @@ +class ExecaError extends Error { + constructor(message = "Command failed", options = {}) { + super(message) + this.name = "ExecaError" + this.exitCode = options.exitCode ?? 1 + this.signal = options.signal + } +} + +const mockFn = (implementation) => (typeof jest === "undefined" ? implementation : jest.fn(implementation)) + +const createSubprocess = () => { + const subprocess = Promise.resolve({ + exitCode: 0, + stdout: "", + stderr: "", + all: "", + }) + + subprocess.pid = 1234 + subprocess.iterable = mockFn(async function* () {}) + subprocess.kill = mockFn(() => true) + + return subprocess +} + +const execa = mockFn((firstArg) => { + if (firstArg && typeof firstArg === "object" && !Array.isArray(firstArg)) { + return mockFn(() => createSubprocess()) + } + + return createSubprocess() +}) + +module.exports = { + execa, + ExecaError, +} diff --git a/src/__mocks__/vscode.js b/src/__mocks__/vscode.js index c40d6dc680c..ae8d51c2236 100644 --- a/src/__mocks__/vscode.js +++ b/src/__mocks__/vscode.js @@ -60,6 +60,10 @@ const vscode = { Workspace: 2, WorkspaceFolder: 3, }, + CodeActionKind: { + QuickFix: { value: "quickfix" }, + RefactorRewrite: { value: "refactor.rewrite" }, + }, Position: class { constructor(line, character) { this.line = line diff --git a/src/api/providers/__tests__/deepseek.test.ts b/src/api/providers/__tests__/deepseek.test.ts index eb00bf6d65d..6f2aef12553 100644 --- a/src/api/providers/__tests__/deepseek.test.ts +++ b/src/api/providers/__tests__/deepseek.test.ts @@ -165,13 +165,17 @@ describe("DeepSeekHandler", () => { ...mockOptions, apiModelId: "invalid-model", }) + const defaultHandler = new DeepSeekHandler({ + ...mockOptions, + apiModelId: undefined, + }) const model = handlerWithInvalidModel.getModel() expect(model.id).toBe("invalid-model") // Returns provided ID expect(model.info).toBeDefined() // With the current implementation, it's the same object reference when using default model info - expect(model.info).toBe(handler.getModel().info) + expect(model.info).toBe(defaultHandler.getModel().info) // Should have the same base properties - expect(model.info.contextWindow).toBe(handler.getModel().info.contextWindow) + expect(model.info.contextWindow).toBe(defaultHandler.getModel().info.contextWindow) // And should have supportsPromptCache set to true expect(model.info.supportsPromptCache).toBe(true) }) diff --git a/src/api/providers/__tests__/openai-native.test.ts b/src/api/providers/__tests__/openai-native.test.ts index 68ab0f5a5fa..c36696e5a05 100644 --- a/src/api/providers/__tests__/openai-native.test.ts +++ b/src/api/providers/__tests__/openai-native.test.ts @@ -383,7 +383,7 @@ describe("OpenAiNativeHandler", () => { openAiNativeApiKey: "test-api-key", }) const modelInfo = handlerWithoutModel.getModel() - expect(modelInfo.id).toBe("gpt-4.1") // Default model + expect(modelInfo.id).toBe("gpt-5.5") // Default model expect(modelInfo.info).toBeDefined() }) }) diff --git a/src/api/providers/__tests__/xai.test.ts b/src/api/providers/__tests__/xai.test.ts index f17e75277cd..7d6fc1d7f24 100644 --- a/src/api/providers/__tests__/xai.test.ts +++ b/src/api/providers/__tests__/xai.test.ts @@ -61,7 +61,7 @@ describe("XAIHandler", () => { }) test("should return specified model when valid model is provided", () => { - const testModelId = "grok-2-latest" + const testModelId = "grok-build-0.1" const handlerWithModel = new XAIHandler({ apiModelId: testModelId }) const model = handlerWithModel.getModel() @@ -69,9 +69,9 @@ describe("XAIHandler", () => { expect(model.info).toEqual(xaiModels[testModelId]) }) - test("should include reasoning_effort parameter for mini models", async () => { - const miniModelHandler = new XAIHandler({ - apiModelId: "grok-3-mini-beta", + test("should include reasoning_effort parameter for Grok 4.3", async () => { + const reasoningModelHandler = new XAIHandler({ + apiModelId: "grok-4.3", reasoningEffort: "high", }) @@ -87,7 +87,7 @@ describe("XAIHandler", () => { }) // Start generating a message - const messageGenerator = miniModelHandler.createMessage("test prompt", []) + const messageGenerator = reasoningModelHandler.createMessage("test prompt", []) await messageGenerator.next() // Start the generator // Check that reasoning_effort was included @@ -98,9 +98,9 @@ describe("XAIHandler", () => { ) }) - test("should not include reasoning_effort parameter for non-mini models", async () => { + test("should not include reasoning_effort parameter for non-reasoning models", async () => { const regularModelHandler = new XAIHandler({ - apiModelId: "grok-2-latest", + apiModelId: "grok-build-0.1", reasoningEffort: "high", }) @@ -254,7 +254,7 @@ describe("XAIHandler", () => { test("createMessage should pass correct parameters to OpenAI client", async () => { // Setup a handler with specific model - const modelId = "grok-2-latest" + const modelId = "grok-build-0.1" const modelInfo = xaiModels[modelId] const handlerWithModel = new XAIHandler({ apiModelId: modelId }) diff --git a/src/api/providers/anthropic.ts b/src/api/providers/anthropic.ts index b65e89e0bb3..5e490e29ae9 100644 --- a/src/api/providers/anthropic.ts +++ b/src/api/providers/anthropic.ts @@ -13,6 +13,12 @@ import { BaseProvider } from "./base-provider" import { ANTHROPIC_DEFAULT_MAX_TOKENS } from "./constants" import { SingleCompletionHandler, getModelParams } from "../index" +const ANTHROPIC_MODELS_WITHOUT_SAMPLING_PARAMS = new Set(["claude-fable-5", "claude-opus-4-8", "claude-sonnet-4-6"]) + +function omitsSamplingParams(modelId: string) { + return ANTHROPIC_MODELS_WITHOUT_SAMPLING_PARAMS.has(modelId) || modelId.startsWith("claude-opus-4-7") +} + export class AnthropicHandler extends BaseProvider implements SingleCompletionHandler { private options: ApiHandlerOptions private client: Anthropic @@ -34,8 +40,16 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa let stream: AnthropicStream const cacheControl: CacheControlEphemeral = { type: "ephemeral" } let { id: modelId, maxTokens, thinking, temperature, virtualId } = this.getModel() + let requestTemperature: number | undefined = temperature + if (omitsSamplingParams(modelId)) { + requestTemperature = undefined + } switch (modelId) { + case "claude-fable-5": + case "claude-opus-4-8": + case "claude-sonnet-4-6": + case "claude-haiku-4-5-20251001": case "claude-3-7-sonnet-20250219": case "claude-3-5-sonnet-20241022": case "claude-3-5-haiku-20241022": @@ -63,7 +77,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa { model: modelId, max_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS, - temperature, + temperature: requestTemperature, thinking, // Setting cache breakpoint for system prompt so new tasks can reuse it. system: [{ text: systemPrompt, type: "text", cache_control: cacheControl }], @@ -129,7 +143,7 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa stream = (await this.client.messages.create({ model: modelId, max_tokens: maxTokens ?? ANTHROPIC_DEFAULT_MAX_TOKENS, - temperature, + temperature: requestTemperature, system: [{ text: systemPrompt, type: "text" }], messages, stream: true, @@ -273,12 +287,16 @@ export class AnthropicHandler extends BaseProvider implements SingleCompletionHa async completePrompt(prompt: string) { let { id: model, temperature } = this.getModel() + let requestTemperature: number | undefined = temperature + if (omitsSamplingParams(model)) { + requestTemperature = undefined + } const message = await this.client.messages.create({ model, max_tokens: ANTHROPIC_DEFAULT_MAX_TOKENS, thinking: undefined, - temperature, + temperature: requestTemperature, messages: [{ role: "user", content: prompt }], stream: false, }) diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index 62782b3d4f9..f4c4b3baded 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -45,6 +45,16 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio return } + if (this.shouldUseResponsesApi(model.id)) { + yield* this.handleResponsesMessage(model, systemPrompt, messages) + return + } + + if (model.id.startsWith("gpt-5")) { + yield* this.handleGpt5FamilyMessage(model, systemPrompt, messages) + return + } + if (model.id.startsWith("o3")) { yield* this.handleReasonerMessage(model, "o3", systemPrompt, messages) return @@ -99,7 +109,54 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio ], stream: true, stream_options: { include_usage: true }, - reasoning_effort: this.getModel().info.reasoningEffort, + reasoning_effort: this.getChatReasoningEffort(), + }) + + yield* this.handleStreamResponse(stream, model) + } + + private async *handleResponsesMessage( + model: OpenAiNativeModel, + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + ): ApiStream { + const response = await this.client.responses.create({ + model: model.id as OpenAI.Responses.ResponseCreateParamsNonStreaming["model"], + instructions: systemPrompt, + input: this.convertMessagesToResponsesInput(messages), + max_output_tokens: model.info.maxTokens, + reasoning: this.getResponsesReasoning(), + stream: false, + }) + + yield { + type: "text", + text: response.output_text || "", + } + + if (response.usage) { + yield* this.yieldUsage(model.info, { + prompt_tokens: response.usage.input_tokens, + completion_tokens: response.usage.output_tokens, + total_tokens: response.usage.total_tokens, + prompt_tokens_details: { + cached_tokens: response.usage.input_tokens_details?.cached_tokens ?? 0, + }, + } as OpenAI.Completions.CompletionUsage) + } + } + + private async *handleGpt5FamilyMessage( + model: OpenAiNativeModel, + systemPrompt: string, + messages: Anthropic.Messages.MessageParam[], + ): ApiStream { + const stream = await this.client.chat.completions.create({ + model: model.id, + messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)], + stream: true, + stream_options: { include_usage: true }, + reasoning_effort: this.getChatReasoningEffort(), }) yield* this.handleStreamResponse(stream, model) @@ -178,6 +235,79 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio return { id: openAiNativeDefaultModelId, info: openAiNativeModels[openAiNativeDefaultModelId] } } + private shouldUseResponsesApi(modelId: string): boolean { + return modelId === "gpt-5.5-pro" || modelId === "gpt-5.4-pro" + } + + private getChatReasoningEffort(): OpenAI.Chat.ChatCompletionReasoningEffort | undefined { + return this.getModel().info.reasoningEffort as OpenAI.Chat.ChatCompletionReasoningEffort | undefined + } + + private getResponsesReasoning(): OpenAI.Responses.ResponseCreateParamsNonStreaming["reasoning"] | undefined { + const reasoningEffort = this.getModel().info.reasoningEffort + return reasoningEffort ? { effort: reasoningEffort as any } : undefined + } + + private convertMessagesToResponsesInput( + messages: Anthropic.Messages.MessageParam[], + ): OpenAI.Responses.ResponseInput { + return convertToOpenAiMessages(messages).map((message) => { + if (message.role === "tool") { + return { + role: "user", + content: `Tool result (${message.tool_call_id}): ${message.content}`, + } + } + + const role = + message.role === "system" || message.role === "developer" || message.role === "assistant" + ? message.role + : "user" + const toolCallText = + "tool_calls" in message && message.tool_calls?.length + ? message.tool_calls + .map((toolCall) => + "function" in toolCall + ? `Tool call ${toolCall.function.name}: ${toolCall.function.arguments}` + : `Tool call ${toolCall.id}`, + ) + .join("\n") + : undefined + + if (typeof message.content === "string" || message.content == null) { + return { + role, + content: [message.content, toolCallText].filter(Boolean).join("\n"), + } + } + + return { + role, + content: message.content.map((part): OpenAI.Responses.ResponseInputContent => { + if (part.type === "image_url") { + return { + type: "input_image", + image_url: part.image_url.url, + detail: "auto", + } + } + + if (part.type === "text") { + return { + type: "input_text", + text: part.text, + } + } + + return { + type: "input_text", + text: "", + } + }), + } + }) + } + async completePrompt(prompt: string): Promise { try { const model = this.getModel() @@ -187,6 +317,10 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio requestOptions = this.getO1CompletionOptions(model, prompt) } else if (model.id.startsWith("o3-mini")) { requestOptions = this.getO3CompletionOptions(model, prompt) + } else if (this.shouldUseResponsesApi(model.id)) { + return this.completePromptWithResponsesApi(model, prompt) + } else if (model.id.startsWith("gpt-5")) { + requestOptions = this.getGpt5CompletionOptions(model, prompt) } else { requestOptions = this.getDefaultCompletionOptions(model, prompt) } @@ -218,10 +352,33 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio return { model: "o3-mini", messages: [{ role: "user", content: prompt }], - reasoning_effort: this.getModel().info.reasoningEffort, + reasoning_effort: this.getChatReasoningEffort(), + } + } + + private getGpt5CompletionOptions( + model: OpenAiNativeModel, + prompt: string, + ): OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming { + return { + model: model.id, + messages: [{ role: "user", content: prompt }], + reasoning_effort: this.getChatReasoningEffort(), } } + private async completePromptWithResponsesApi(model: OpenAiNativeModel, prompt: string): Promise { + const response = await this.client.responses.create({ + model: model.id as OpenAI.Responses.ResponseCreateParamsNonStreaming["model"], + input: prompt, + max_output_tokens: model.info.maxTokens, + reasoning: this.getResponsesReasoning(), + stream: false, + }) + + return response.output_text || "" + } + private getDefaultCompletionOptions( model: OpenAiNativeModel, prompt: string, diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 64932b03921..4ab1bb6d137 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -145,7 +145,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl messages: convertedMessages, stream: true as const, ...(isGrokXAI ? {} : { stream_options: { include_usage: true } }), - reasoning_effort: this.getModel().info.reasoningEffort, + reasoning_effort: this.getChatReasoningEffort(), } if (this.options.includeMaxTokens) { @@ -288,7 +288,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl ], stream: true, ...(isGrokXAI ? {} : { stream_options: { include_usage: true } }), - reasoning_effort: this.getModel().info.reasoningEffort, + reasoning_effort: this.getChatReasoningEffort(), }, methodIsAzureAiInference ? { path: AZURE_AI_INFERENCE_PATH } : {}, ) @@ -358,6 +358,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl const urlHost = this._getUrlHost(baseUrl) return urlHost.endsWith(".services.ai.azure.com") } + + private getChatReasoningEffort(): OpenAI.Chat.ChatCompletionReasoningEffort | undefined { + return this.getModel().info.reasoningEffort as OpenAI.Chat.ChatCompletionReasoningEffort | undefined + } } export async function getOpenAiModels(baseUrl?: string, apiKey?: string, openAiHeaders?: Record) { diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index e1104f4f9a5..d9408dcf853 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -25,19 +25,36 @@ import { getModels } from "./fetchers/cache" const OPENROUTER_DEFAULT_PROVIDER_NAME = "[default]" -// Add custom interface for OpenRouter params. -type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & { +type OpenRouterReasoningEffort = "high" | "medium" | "low" + +type OpenRouterExtraParams = { transforms?: string[] include_reasoning?: boolean thinking?: BetaThinkingConfigParam // https://openrouter.ai/docs/use-cases/reasoning-tokens reasoning?: { - effort?: "high" | "medium" | "low" + effort?: OpenRouterReasoningEffort max_tokens?: number exclude?: boolean } } +type OpenRouterChatCompletionStreamingParams = OpenAI.Chat.ChatCompletionCreateParamsStreaming & OpenRouterExtraParams +type OpenRouterChatCompletionNonStreamingParams = OpenAI.Chat.ChatCompletionCreateParamsNonStreaming & + OpenRouterExtraParams + +const toOpenRouterReasoningEffort = (effort?: string): OpenRouterReasoningEffort | undefined => { + if (effort === "low" || effort === "medium" || effort === "high") { + return effort + } + + if (effort === "xhigh") { + return "high" + } + + return undefined +} + // See `OpenAI.Chat.Completions.ChatCompletionChunk["usage"]` // `CompletionsAPI.CompletionUsage` // See also: https://openrouter.ai/docs/use-cases/usage-accounting @@ -104,7 +121,8 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH } // https://openrouter.ai/docs/transforms - const completionParams: OpenRouterChatCompletionParams = { + const openRouterReasoningEffort = toOpenRouterReasoningEffort(reasoningEffort) + const completionParams: OpenRouterChatCompletionStreamingParams = { model: modelId, max_tokens: maxTokens, temperature, @@ -120,7 +138,10 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH }), // This way, the transforms field will only be included in the parameters when openRouterUseMiddleOutTransform is true. ...((this.options.openRouterUseMiddleOutTransform ?? true) && { transforms: ["middle-out"] }), - ...(REASONING_MODELS.has(modelId) && reasoningEffort && { reasoning: { effort: reasoningEffort } }), + ...(REASONING_MODELS.has(modelId) && + openRouterReasoningEffort && { + reasoning: { effort: openRouterReasoningEffort }, + }), } const stream = await this.client.chat.completions.create(completionParams) @@ -195,7 +216,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH async completePrompt(prompt: string) { let { id: modelId, maxTokens, thinking, temperature } = await this.fetchModel() - const completionParams: OpenRouterChatCompletionParams = { + const completionParams: OpenRouterChatCompletionNonStreamingParams = { model: modelId, max_tokens: maxTokens, thinking, diff --git a/src/api/providers/pearai/pearaiGeneric.ts b/src/api/providers/pearai/pearaiGeneric.ts index 4fedb252cae..ac6499f30b7 100644 --- a/src/api/providers/pearai/pearaiGeneric.ts +++ b/src/api/providers/pearai/pearaiGeneric.ts @@ -279,7 +279,7 @@ export class PearAIGenericHandler extends BaseProvider implements SingleCompleti ], stream: true, stream_options: { include_usage: true }, - reasoning_effort: this.getModel().info.reasoningEffort, + reasoning_effort: this.getChatReasoningEffort(), }) yield* this.handleStreamResponse(stream) @@ -325,6 +325,10 @@ export class PearAIGenericHandler extends BaseProvider implements SingleCompleti } } } + + private getChatReasoningEffort(): OpenAI.Chat.ChatCompletionReasoningEffort | undefined { + return this.getModel().info.reasoningEffort as OpenAI.Chat.ChatCompletionReasoningEffort | undefined + } } export async function getOpenAiModels(baseUrl?: string, apiKey?: string) { diff --git a/src/api/providers/xai.ts b/src/api/providers/xai.ts index 6425dd03179..ba1a02d56db 100644 --- a/src/api/providers/xai.ts +++ b/src/api/providers/xai.ts @@ -11,6 +11,24 @@ import { BaseProvider } from "./base-provider" const XAI_DEFAULT_TEMPERATURE = 0 +const toXAIReasoningEffort = ( + effort?: ApiHandlerOptions["reasoningEffort"], +): OpenAI.Chat.ChatCompletionReasoningEffort | "none" | undefined => { + if (effort === "none") { + return "none" + } + + if (effort === "low" || effort === "medium" || effort === "high") { + return effort + } + + if (effort === "xhigh") { + return "high" + } + + return undefined +} + export class XAIHandler extends BaseProvider implements SingleCompletionHandler { protected options: ApiHandlerOptions private client: OpenAI @@ -38,7 +56,7 @@ export class XAIHandler extends BaseProvider implements SingleCompletionHandler return { id, info: xaiModels[id], - reasoningEffort: supportsReasoning ? this.options.reasoningEffort : undefined, + reasoningEffort: supportsReasoning ? toXAIReasoningEffort(this.options.reasoningEffort) : undefined, } } diff --git a/src/core/CodeActionProvider.ts b/src/core/CodeActionProvider.ts index f749987d7e6..0b0a8e15de3 100644 --- a/src/core/CodeActionProvider.ts +++ b/src/core/CodeActionProvider.ts @@ -27,11 +27,15 @@ export const COMMAND_IDS: Record = { NEW_TASK: "roo-cline.newTask", } as const +const codeActionKind = { + QuickFix: ((vscode as any).CodeActionKind?.QuickFix ?? { value: "quickfix" }) as vscode.CodeActionKind, + RefactorRewrite: ((vscode as any).CodeActionKind?.RefactorRewrite ?? { + value: "refactor.rewrite", + }) as vscode.CodeActionKind, +} + export class CodeActionProvider implements vscode.CodeActionProvider { - public static readonly providedCodeActionKinds = [ - vscode.CodeActionKind.QuickFix, - vscode.CodeActionKind.RefactorRewrite, - ] + public static readonly providedCodeActionKinds = [codeActionKind.QuickFix, codeActionKind.RefactorRewrite] private createAction( title: string, @@ -60,17 +64,12 @@ export class CodeActionProvider implements vscode.CodeActionProvider { const actions: vscode.CodeAction[] = [] actions.push( - this.createAction( - ACTION_TITLES.ADD_TO_CONTEXT, - vscode.CodeActionKind.QuickFix, - COMMAND_IDS.ADD_TO_CONTEXT, - [ - filePath, - effectiveRange.text, - effectiveRange.range.start.line + 1, - effectiveRange.range.end.line + 1, - ], - ), + this.createAction(ACTION_TITLES.ADD_TO_CONTEXT, codeActionKind.QuickFix, COMMAND_IDS.ADD_TO_CONTEXT, [ + filePath, + effectiveRange.text, + effectiveRange.range.start.line + 1, + effectiveRange.range.end.line + 1, + ]), ) if (context.diagnostics.length > 0) { @@ -80,7 +79,7 @@ export class CodeActionProvider implements vscode.CodeActionProvider { if (relevantDiagnostics.length > 0) { actions.push( - this.createAction(ACTION_TITLES.FIX, vscode.CodeActionKind.QuickFix, COMMAND_IDS.FIX, [ + this.createAction(ACTION_TITLES.FIX, codeActionKind.QuickFix, COMMAND_IDS.FIX, [ filePath, effectiveRange.text, effectiveRange.range.start.line + 1, @@ -91,7 +90,7 @@ export class CodeActionProvider implements vscode.CodeActionProvider { } } else { actions.push( - this.createAction(ACTION_TITLES.EXPLAIN, vscode.CodeActionKind.QuickFix, COMMAND_IDS.EXPLAIN, [ + this.createAction(ACTION_TITLES.EXPLAIN, codeActionKind.QuickFix, COMMAND_IDS.EXPLAIN, [ filePath, effectiveRange.text, effectiveRange.range.start.line + 1, @@ -100,7 +99,7 @@ export class CodeActionProvider implements vscode.CodeActionProvider { ) actions.push( - this.createAction(ACTION_TITLES.IMPROVE, vscode.CodeActionKind.QuickFix, COMMAND_IDS.IMPROVE, [ + this.createAction(ACTION_TITLES.IMPROVE, codeActionKind.QuickFix, COMMAND_IDS.IMPROVE, [ filePath, effectiveRange.text, effectiveRange.range.start.line + 1, diff --git a/src/core/__tests__/Cline.test.ts b/src/core/__tests__/Cline.test.ts index 00a9c4dc6bf..5cf32856923 100644 --- a/src/core/__tests__/Cline.test.ts +++ b/src/core/__tests__/Cline.test.ts @@ -401,16 +401,14 @@ describe("Cline", () => { }) it("should clean conversation history before sending to API", async () => { - // Cline.create will now use our mocked getEnvironmentDetails - const [cline, task] = Cline.create({ + const cline = new Cline({ provider: mockProvider, apiConfiguration: mockApiConfig, task: "test task", + startTask: false, + enableCheckpoints: false, }) - cline.abandoned = true - await task - // Set up mock stream. const mockStreamForClean = (async function* () { yield { type: "text", text: "test response" } diff --git a/src/core/__tests__/mode-validator.test.ts b/src/core/__tests__/mode-validator.test.ts index 1111f24b9f2..03ae98d8484 100644 --- a/src/core/__tests__/mode-validator.test.ts +++ b/src/core/__tests__/mode-validator.test.ts @@ -4,7 +4,9 @@ import { isToolAllowedForMode, modes, ModeConfig } from "../../shared/modes" import { TOOL_GROUPS } from "../../shared/tools" import { validateToolUse } from "../mode-validator" -const [codeMode, architectMode, askMode] = modes.map((mode) => mode.slug) +const codeMode = modes.find((mode) => mode.slug === "code")!.slug +const architectMode = modes.find((mode) => mode.slug === "architect")!.slug +const askMode = modes.find((mode) => mode.slug === "ask")!.slug describe("mode-validator", () => { describe("isToolAllowedForMode", () => { diff --git a/src/core/config/__tests__/CustomModesManager.test.ts b/src/core/config/__tests__/CustomModesManager.test.ts index dabf4e4bcb7..0509daa5b46 100644 --- a/src/core/config/__tests__/CustomModesManager.test.ts +++ b/src/core/config/__tests__/CustomModesManager.test.ts @@ -23,7 +23,7 @@ describe("CustomModesManager", () => { // Use path.sep to ensure correct path separators for the current platform const mockStoragePath = `${path.sep}mock${path.sep}settings` const mockSettingsPath = path.join(mockStoragePath, "settings", GlobalFileNames.customModes) - const mockRoomodes = `${path.sep}mock${path.sep}workspace${path.sep}.pearai-agent-ignore` + const mockRoomodes = `${path.sep}mock${path.sep}workspace${path.sep}.pearai-agent-modes` beforeEach(() => { mockOnUpdate = jest.fn() @@ -60,7 +60,7 @@ describe("CustomModesManager", () => { }) describe("getCustomModes", () => { - it("should merge modes with .pearai-agent-ignore taking precedence", async () => { + it("should merge modes with .pearai-agent-modes taking precedence", async () => { const settingsModes = [ { slug: "mode1", name: "Mode 1", roleDefinition: "Role 1", groups: ["read"] }, { slug: "mode2", name: "Mode 2", roleDefinition: "Role 2", groups: ["read"] }, @@ -87,13 +87,13 @@ describe("CustomModesManager", () => { expect(modes).toHaveLength(3) expect(modes.map((m) => m.slug)).toEqual(["mode2", "mode3", "mode1"]) - // mode2 should come from .pearai-agent-ignore since it takes precedence + // mode2 should come from .pearai-agent-modes since it takes precedence const mode2 = modes.find((m) => m.slug === "mode2") expect(mode2?.name).toBe("Mode 2 Override") expect(mode2?.roleDefinition).toBe("Role 2 Override") }) - it("should handle missing .pearai-agent-ignore file", async () => { + it("should handle missing .pearai-agent-modes file", async () => { const settingsModes = [{ slug: "mode1", name: "Mode 1", roleDefinition: "Role 1", groups: ["read"] }] ;(fileExistsAtPath as jest.Mock).mockImplementation(async (path: string) => { @@ -112,7 +112,7 @@ describe("CustomModesManager", () => { expect(modes[0].slug).toBe("mode1") }) - it("should handle invalid JSON in .pearai-agent-ignore", async () => { + it("should handle invalid JSON in .pearai-agent-modes", async () => { const settingsModes = [{ slug: "mode1", name: "Mode 1", roleDefinition: "Role 1", groups: ["read"] }] ;(fs.readFile as jest.Mock).mockImplementation(async (path: string) => { @@ -127,7 +127,7 @@ describe("CustomModesManager", () => { const modes = await manager.getCustomModes() - // Should fall back to settings modes when .pearai-agent-ignore is invalid + // Should fall back to settings modes when .pearai-agent-modes is invalid expect(modes).toHaveLength(1) expect(modes[0].slug).toBe("mode1") }) @@ -385,7 +385,7 @@ describe("CustomModesManager", () => { }) describe("updateCustomMode", () => { - it("should update mode in settings file while preserving .pearai-agent-ignore precedence", async () => { + it("should update mode in settings file while preserving .pearai-agent-modes precedence", async () => { const newMode: ModeConfig = { slug: "mode1", name: "Updated Mode 1", @@ -449,13 +449,13 @@ describe("CustomModesManager", () => { }), ) - // Should update global state with merged modes where .pearai-agent-ignore takes precedence + // Should update global state with merged modes where .pearai-agent-modes takes precedence expect(mockContext.globalState.update).toHaveBeenCalledWith( "customModes", expect.arrayContaining([ expect.objectContaining({ slug: "mode1", - name: "Roomodes Mode 1", // .pearai-agent-ignore version should take precedence + name: "Roomodes Mode 1", // .pearai-agent-modes version should take precedence source: "project", }), ]), @@ -465,7 +465,7 @@ describe("CustomModesManager", () => { expect(mockOnUpdate).toHaveBeenCalled() }) - it("creates .pearai-agent-ignore file when adding project-specific mode", async () => { + it("creates .pearai-agent-modes file when adding project-specific mode", async () => { const projectMode: ModeConfig = { slug: "project-mode", name: "Project Mode", @@ -474,7 +474,7 @@ describe("CustomModesManager", () => { source: "project", } - // Mock .pearai-agent-ignore to not exist initially + // Mock .pearai-agent-modes to not exist initially let roomodesContent: any = null ;(fileExistsAtPath as jest.Mock).mockImplementation(async (path: string) => { return path === mockSettingsPath @@ -500,7 +500,7 @@ describe("CustomModesManager", () => { await manager.updateCustomMode("project-mode", projectMode) - // Verify .pearai-agent-ignore was created with the project mode + // Verify .pearai-agent-modes was created with the project mode expect(fs.writeFile).toHaveBeenCalledWith( expect.any(String), // Don't check exact path as it may have different separators on different platforms expect.stringContaining("project-mode"), @@ -511,7 +511,7 @@ describe("CustomModesManager", () => { const writeCall = (fs.writeFile as jest.Mock).mock.calls[0] expect(path.normalize(writeCall[0])).toBe(path.normalize(mockRoomodes)) - // Verify the content written to .pearai-agent-ignore + // Verify the content written to .pearai-agent-modes expect(roomodesContent).toEqual({ customModes: [ expect.objectContaining({ diff --git a/src/core/config/__tests__/importExport.test.ts b/src/core/config/__tests__/importExport.test.ts index eef83959cc6..e99c209093a 100644 --- a/src/core/config/__tests__/importExport.test.ts +++ b/src/core/config/__tests__/importExport.test.ts @@ -31,6 +31,7 @@ jest.mock("fs/promises", () => ({ // Mock os module jest.mock("os", () => ({ + ...jest.requireActual("os"), homedir: jest.fn(() => "/mock/home"), })) diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index 7c07f06ba0b..15e9d75fe88 100644 --- a/src/core/diff/strategies/multi-search-replace.ts +++ b/src/core/diff/strategies/multi-search-replace.ts @@ -163,7 +163,7 @@ def calculate_sum(items): return total ======= sum += item - return sum + return sum >>>>>>> REPLACE \`\`\` diff --git a/src/core/ignore/__tests__/RooIgnoreController.test.ts b/src/core/ignore/__tests__/RooIgnoreController.test.ts index 1e5dbd50727..0b154317942 100644 --- a/src/core/ignore/__tests__/RooIgnoreController.test.ts +++ b/src/core/ignore/__tests__/RooIgnoreController.test.ts @@ -70,10 +70,10 @@ describe("RooIgnoreController", () => { describe("initialization", () => { /** - * Tests the controller initialization when .rooignore exists + * Tests the controller initialization when .pearai-agent-ignore exists */ - it("should load .rooignore patterns on initialization when file exists", async () => { - // Setup mocks to simulate existing .rooignore file + it("should load .pearai-agent-ignore patterns on initialization when file exists", async () => { + // Setup mocks to simulate existing .pearai-agent-ignore file mockFileExists.mockResolvedValue(true) mockReadFile.mockResolvedValue("node_modules\n.git\nsecrets.json") @@ -81,8 +81,8 @@ describe("RooIgnoreController", () => { await controller.initialize() // Verify file was checked and read - expect(mockFileExists).toHaveBeenCalledWith(path.join(TEST_CWD, ".rooignore")) - expect(mockReadFile).toHaveBeenCalledWith(path.join(TEST_CWD, ".rooignore"), "utf8") + expect(mockFileExists).toHaveBeenCalledWith(path.join(TEST_CWD, ".pearai-agent-ignore")) + expect(mockReadFile).toHaveBeenCalledWith(path.join(TEST_CWD, ".pearai-agent-ignore"), "utf8") // Verify content was stored expect(controller.rooIgnoreContent).toBe("node_modules\n.git\nsecrets.json") @@ -95,10 +95,10 @@ describe("RooIgnoreController", () => { }) /** - * Tests the controller behavior when .rooignore doesn't exist + * Tests the controller behavior when .pearai-agent-ignore doesn't exist */ - it("should allow all access when .rooignore doesn't exist", async () => { - // Setup mocks to simulate missing .rooignore file + it("should allow all access when .pearai-agent-ignore doesn't exist", async () => { + // Setup mocks to simulate missing .pearai-agent-ignore file mockFileExists.mockResolvedValue(false) // Initialize controller @@ -115,12 +115,12 @@ describe("RooIgnoreController", () => { /** * Tests the file watcher setup */ - it("should set up file watcher for .rooignore changes", async () => { + it("should set up file watcher for .pearai-agent-ignore changes", async () => { // Check that watcher was created with correct pattern expect(vscode.workspace.createFileSystemWatcher).toHaveBeenCalledWith( expect.objectContaining({ base: TEST_CWD, - pattern: ".rooignore", + pattern: ".pearai-agent-ignore", }), ) @@ -133,7 +133,7 @@ describe("RooIgnoreController", () => { /** * Tests error handling during initialization */ - it("should handle errors when loading .rooignore", async () => { + it("should handle errors when loading .pearai-agent-ignore", async () => { // Setup mocks to simulate error mockFileExists.mockResolvedValue(true) mockReadFile.mockRejectedValue(new Error("Test file read error")) @@ -145,7 +145,7 @@ describe("RooIgnoreController", () => { await controller.initialize() // Verify error was logged - expect(consoleSpy).toHaveBeenCalledWith("Unexpected error loading .rooignore:", expect.any(Error)) + expect(consoleSpy).toHaveBeenCalledWith("Unexpected error loading .pearai-agent-ignore:", expect.any(Error)) // Cleanup consoleSpy.mockRestore() @@ -154,7 +154,7 @@ describe("RooIgnoreController", () => { describe("validateAccess", () => { beforeEach(async () => { - // Setup .rooignore content + // Setup .pearai-agent-ignore content mockFileExists.mockResolvedValue(true) mockReadFile.mockResolvedValue("node_modules\n.git\nsecrets/**\n*.log") await controller.initialize() @@ -202,10 +202,10 @@ describe("RooIgnoreController", () => { }) /** - * Tests the default behavior when no .rooignore exists + * Tests the default behavior when no .pearai-agent-ignore exists */ - it("should allow all access when no .rooignore content", async () => { - // Create a new controller with no .rooignore + it("should allow all access when no .pearai-agent-ignore content", async () => { + // Create a new controller with no .pearai-agent-ignore mockFileExists.mockResolvedValue(false) const emptyController = new RooIgnoreController(TEST_CWD) await emptyController.initialize() @@ -219,7 +219,7 @@ describe("RooIgnoreController", () => { describe("validateCommand", () => { beforeEach(async () => { - // Setup .rooignore content + // Setup .pearai-agent-ignore content mockFileExists.mockResolvedValue(true) mockReadFile.mockResolvedValue("node_modules\n.git\nsecrets/**\n*.log") await controller.initialize() @@ -274,10 +274,10 @@ describe("RooIgnoreController", () => { }) /** - * Tests behavior when no .rooignore exists + * Tests behavior when no .pearai-agent-ignore exists */ - it("should allow all commands when no .rooignore exists", async () => { - // Create a new controller with no .rooignore + it("should allow all commands when no .pearai-agent-ignore exists", async () => { + // Create a new controller with no .pearai-agent-ignore mockFileExists.mockResolvedValue(false) const emptyController = new RooIgnoreController(TEST_CWD) await emptyController.initialize() @@ -290,7 +290,7 @@ describe("RooIgnoreController", () => { describe("filterPaths", () => { beforeEach(async () => { - // Setup .rooignore content + // Setup .pearai-agent-ignore content mockFileExists.mockResolvedValue(true) mockReadFile.mockResolvedValue("node_modules\n.git\nsecrets/**\n*.log") await controller.initialize() @@ -353,10 +353,10 @@ describe("RooIgnoreController", () => { describe("getInstructions", () => { /** - * Tests instructions generation with .rooignore + * Tests instructions generation with .pearai-agent-ignore */ - it("should generate formatted instructions when .rooignore exists", async () => { - // Setup .rooignore content + it("should generate formatted instructions when .pearai-agent-ignore exists", async () => { + // Setup .pearai-agent-ignore content mockFileExists.mockResolvedValue(true) mockReadFile.mockResolvedValue("node_modules\n.git\nsecrets/**") await controller.initialize() @@ -364,7 +364,7 @@ describe("RooIgnoreController", () => { const instructions = controller.getInstructions() // Verify instruction format - expect(instructions).toContain("# .rooignore") + expect(instructions).toContain("# .pearai-agent-ignore") expect(instructions).toContain(LOCK_TEXT_SYMBOL) expect(instructions).toContain("node_modules") expect(instructions).toContain(".git") @@ -372,10 +372,10 @@ describe("RooIgnoreController", () => { }) /** - * Tests behavior when no .rooignore exists + * Tests behavior when no .pearai-agent-ignore exists */ - it("should return undefined when no .rooignore exists", async () => { - // Setup no .rooignore + it("should return undefined when no .pearai-agent-ignore exists", async () => { + // Setup no .pearai-agent-ignore mockFileExists.mockResolvedValue(false) await controller.initialize() @@ -408,10 +408,10 @@ describe("RooIgnoreController", () => { describe("file watcher", () => { /** - * Tests behavior when .rooignore is created + * Tests behavior when .pearai-agent-ignore is created */ - it("should reload .rooignore when file is created", async () => { - // Setup initial state without .rooignore + it("should reload .pearai-agent-ignore when file is created", async () => { + // Setup initial state without .pearai-agent-ignore mockFileExists.mockResolvedValue(false) await controller.initialize() @@ -422,7 +422,7 @@ describe("RooIgnoreController", () => { // Setup for the test mockFileExists.mockResolvedValue(false) // Initially no file exists - // Create and initialize controller with no .rooignore + // Create and initialize controller with no .pearai-agent-ignore controller = new RooIgnoreController(TEST_CWD) await controller.initialize() @@ -433,7 +433,7 @@ describe("RooIgnoreController", () => { mockFileExists.mockResolvedValue(true) mockReadFile.mockResolvedValue("node_modules") - // Force reload of .rooignore content manually + // Force reload of .pearai-agent-ignore content manually await controller.initialize() // Now verify content was updated @@ -444,10 +444,10 @@ describe("RooIgnoreController", () => { }) /** - * Tests behavior when .rooignore is changed + * Tests behavior when .pearai-agent-ignore is changed */ - it("should reload .rooignore when file is changed", async () => { - // Setup initial state with .rooignore + it("should reload .pearai-agent-ignore when file is changed", async () => { + // Setup initial state with .pearai-agent-ignore mockFileExists.mockResolvedValue(true) mockReadFile.mockResolvedValue("node_modules") await controller.initialize() @@ -472,10 +472,10 @@ describe("RooIgnoreController", () => { }) /** - * Tests behavior when .rooignore is deleted + * Tests behavior when .pearai-agent-ignore is deleted */ - it("should reset when .rooignore is deleted", async () => { - // Setup initial state with .rooignore + it("should reset when .pearai-agent-ignore is deleted", async () => { + // Setup initial state with .pearai-agent-ignore mockFileExists.mockResolvedValue(true) mockReadFile.mockResolvedValue("node_modules") await controller.initialize() diff --git a/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap b/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap index 34f9fa45b77..09f3f1a0865 100644 --- a/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap +++ b/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap @@ -1,7 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SYSTEM_PROMPT should exclude diff strategy tool description when diffEnabled is false 1`] = ` -"You are PearAI (Powered by Roo Code / Cline), a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. +"You are PearAI Agent (Powered by Roo Code / Cline), a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. ==== @@ -84,19 +84,6 @@ Example: Requesting instructions to create an MCP Server create_mcp_server -## fetch_instructions -Description: Request to fetch instructions to perform a task -Parameters: -- task: (required) The task to get instructions for. This can take the following values: - create_mcp_server - create_mode - -Example: Requesting instructions to create an MCP Server - - -create_mcp_server - - ## search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: @@ -564,19 +551,6 @@ Example: Requesting instructions to create an MCP Server create_mcp_server -## fetch_instructions -Description: Request to fetch instructions to perform a task -Parameters: -- task: (required) The task to get instructions for. This can take the following values: - create_mcp_server - create_mode - -Example: Requesting instructions to create an MCP Server - - -create_mcp_server - - ## search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: @@ -1044,19 +1018,6 @@ Example: Requesting instructions to create an MCP Server create_mcp_server -## fetch_instructions -Description: Request to fetch instructions to perform a task -Parameters: -- task: (required) The task to get instructions for. This can take the following values: - create_mcp_server - create_mode - -Example: Requesting instructions to create an MCP Server - - -create_mcp_server - - ## search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: @@ -1524,19 +1485,6 @@ Example: Requesting instructions to create an MCP Server create_mcp_server -## fetch_instructions -Description: Request to fetch instructions to perform a task -Parameters: -- task: (required) The task to get instructions for. This can take the following values: - create_mcp_server - create_mode - -Example: Requesting instructions to create an MCP Server - - -create_mcp_server - - ## search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: @@ -2060,19 +2008,6 @@ Example: Requesting instructions to create an MCP Server create_mcp_server -## fetch_instructions -Description: Request to fetch instructions to perform a task -Parameters: -- task: (required) The task to get instructions for. This can take the following values: - create_mcp_server - create_mode - -Example: Requesting instructions to create an MCP Server - - -create_mcp_server - - ## search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: @@ -2608,19 +2543,6 @@ Example: Requesting instructions to create an MCP Server create_mcp_server -## fetch_instructions -Description: Request to fetch instructions to perform a task -Parameters: -- task: (required) The task to get instructions for. This can take the following values: - create_mcp_server - create_mode - -Example: Requesting instructions to create an MCP Server - - -create_mcp_server - - ## search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: @@ -3144,19 +3066,6 @@ Example: Requesting instructions to create an MCP Server create_mcp_server -## fetch_instructions -Description: Request to fetch instructions to perform a task -Parameters: -- task: (required) The task to get instructions for. This can take the following values: - create_mcp_server - create_mode - -Example: Requesting instructions to create an MCP Server - - -create_mcp_server - - ## search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: @@ -3231,9 +3140,6 @@ Parameters: Diff format: \`\`\` -<<<<<<< HEAD -[new content to replace with] -======= <<<<<<< SEARCH :start_line: (required) The line number of original content where the search block starts. ------- @@ -3242,7 +3148,6 @@ Diff format: [new content to replace with] >>>>>>> REPLACE ->>>>>>> 6db1c5a6d17bee8d13cf22b76fb827a25ab4ecf2 \`\`\` @@ -3259,11 +3164,6 @@ Original file: Search/Replace content: \`\`\` -<<<<<<< HEAD -def calculate_total(items): - """Calculate total with 10% markup""" - return sum(item * 1.1 for item in items) -======= <<<<<<< SEARCH :start_line:1 ------- @@ -3278,7 +3178,6 @@ def calculate_total(items): return sum(item * 1.1 for item in items) >>>>>>> REPLACE ->>>>>>> 6db1c5a6d17bee8d13cf22b76fb827a25ab4ecf2 \`\`\` Search/Replace content with multi edits: @@ -3724,19 +3623,6 @@ Example: Requesting instructions to create an MCP Server create_mcp_server -## fetch_instructions -Description: Request to fetch instructions to perform a task -Parameters: -- task: (required) The task to get instructions for. This can take the following values: - create_mcp_server - create_mode - -Example: Requesting instructions to create an MCP Server - - -create_mcp_server - - ## search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: @@ -4246,19 +4132,6 @@ Example: Requesting instructions to create an MCP Server create_mcp_server -## fetch_instructions -Description: Request to fetch instructions to perform a task -Parameters: -- task: (required) The task to get instructions for. This can take the following values: - create_mcp_server - create_mode - -Example: Requesting instructions to create an MCP Server - - -create_mcp_server - - ## search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: @@ -4803,19 +4676,6 @@ Example: Requesting instructions to create an MCP Server create_mcp_server -## fetch_instructions -Description: Request to fetch instructions to perform a task -Parameters: -- task: (required) The task to get instructions for. This can take the following values: - create_mcp_server - create_mode - -Example: Requesting instructions to create an MCP Server - - -create_mcp_server - - ## search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: @@ -5274,19 +5134,6 @@ Example: Requesting instructions to create an MCP Server create_mcp_server -## fetch_instructions -Description: Request to fetch instructions to perform a task -Parameters: -- task: (required) The task to get instructions for. This can take the following values: - create_mcp_server - create_mode - -Example: Requesting instructions to create an MCP Server - - -create_mcp_server - - ## search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: @@ -5662,19 +5509,6 @@ Example: Requesting instructions to create an MCP Server create_mcp_server -## fetch_instructions -Description: Request to fetch instructions to perform a task -Parameters: -- task: (required) The task to get instructions for. This can take the following values: - create_mcp_server - create_mode - -Example: Requesting instructions to create an MCP Server - - -create_mcp_server - - ## search_files Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context. Parameters: @@ -6267,7 +6101,7 @@ USER'S CUSTOM INSTRUCTIONS The 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. Mode-specific Instructions: - Custom mode instructions +Custom mode instructions Rules: # Rules from .clinerules-code: diff --git a/src/core/prompts/__tests__/system.test.ts b/src/core/prompts/__tests__/system.test.ts index 7f480dd69d7..626f1b260cf 100644 --- a/src/core/prompts/__tests__/system.test.ts +++ b/src/core/prompts/__tests__/system.test.ts @@ -2,11 +2,13 @@ import * as vscode from "vscode" import { SYSTEM_PROMPT } from "../system" import { McpHub } from "../../../services/mcp/McpHub" -import { defaultModeSlug, modes, Mode, ModeConfig } from "../../../shared/modes" +import { getModeConfig, Mode, ModeConfig } from "../../../shared/modes" import "../../../utils/path" // Import path utils to get access to toPosix string extension. import { addCustomInstructions } from "../sections/custom-instructions" import { MultiSearchReplaceDiffStrategy } from "../../diff/strategies/multi-search-replace" +const testModeSlug: Mode = "code" + // Mock the sections jest.mock("../sections/modes", () => ({ getModesSection: jest.fn().mockImplementation(async () => `====\n\nMODES\n\n- Test modes section`), @@ -49,7 +51,7 @@ __setMockImplementation( // Add mode-specific instructions after if (modeCustomInstructions?.trim()) { - sections.push(`Mode-specific Instructions:\n${modeCustomInstructions}`) + sections.push(`Mode-specific Instructions:\n${modeCustomInstructions.trim()}`) } // Add rules @@ -182,7 +184,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // mcpHub undefined, // diffStrategy undefined, // browserViewportSize - defaultModeSlug, // mode + testModeSlug, // mode undefined, // customModePrompts undefined, // customModes undefined, // globalCustomInstructions @@ -202,7 +204,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // mcpHub undefined, // diffStrategy "1280x800", // browserViewportSize - defaultModeSlug, // mode + testModeSlug, // mode undefined, // customModePrompts undefined, // customModes, undefined, // globalCustomInstructions @@ -224,7 +226,7 @@ describe("SYSTEM_PROMPT", () => { mockMcpHub, // mcpHub undefined, // diffStrategy undefined, // browserViewportSize - defaultModeSlug, // mode + testModeSlug, // mode undefined, // customModePrompts undefined, // customModes, undefined, // globalCustomInstructions @@ -244,7 +246,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // explicitly undefined mcpHub undefined, // diffStrategy undefined, // browserViewportSize - defaultModeSlug, // mode + testModeSlug, // mode undefined, // customModePrompts undefined, // customModes, undefined, // globalCustomInstructions @@ -264,7 +266,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // mcpHub undefined, // diffStrategy "900x600", // different viewport size - defaultModeSlug, // mode + testModeSlug, // mode undefined, // customModePrompts undefined, // customModes, undefined, // globalCustomInstructions @@ -284,7 +286,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // mcpHub new MultiSearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase undefined, // browserViewportSize - defaultModeSlug, // mode + testModeSlug, // mode undefined, // customModePrompts undefined, // customModes undefined, // globalCustomInstructions @@ -305,7 +307,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // mcpHub new MultiSearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase undefined, // browserViewportSize - defaultModeSlug, // mode + testModeSlug, // mode undefined, // customModePrompts undefined, // customModes undefined, // globalCustomInstructions @@ -326,7 +328,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // mcpHub new MultiSearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase undefined, // browserViewportSize - defaultModeSlug, // mode + testModeSlug, // mode undefined, // customModePrompts undefined, // customModes undefined, // globalCustomInstructions @@ -351,7 +353,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // mcpHub undefined, // diffStrategy undefined, // browserViewportSize - defaultModeSlug, // mode + testModeSlug, // mode undefined, // customModePrompts undefined, // customModes undefined, // globalCustomInstructions @@ -409,7 +411,7 @@ describe("SYSTEM_PROMPT", () => { it("should use promptComponent roleDefinition when available", async () => { const customModePrompts = { - [defaultModeSlug]: { + [testModeSlug]: { roleDefinition: "Custom prompt role definition", customInstructions: "Custom prompt instructions", }, @@ -422,7 +424,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // mcpHub undefined, // diffStrategy undefined, // browserViewportSize - defaultModeSlug as Mode, // mode + testModeSlug as Mode, // mode customModePrompts, // customModePrompts undefined, // customModes undefined, // globalCustomInstructions @@ -433,13 +435,13 @@ describe("SYSTEM_PROMPT", () => { // Role definition from promptComponent should be at the top expect(prompt.indexOf("Custom prompt role definition")).toBeLessThan(prompt.indexOf("TOOL USE")) - // Should not contain the default mode's role definition - expect(prompt).not.toContain(modes[0].roleDefinition) + // Should not contain the mode's default role definition when overridden + expect(prompt).not.toContain(getModeConfig(testModeSlug).roleDefinition) }) it("should fallback to modeConfig roleDefinition when promptComponent has no roleDefinition", async () => { const customModePrompts = { - [defaultModeSlug]: { + [testModeSlug]: { customInstructions: "Custom prompt instructions", // No roleDefinition provided }, @@ -452,7 +454,7 @@ describe("SYSTEM_PROMPT", () => { undefined, // mcpHub undefined, // diffStrategy undefined, // browserViewportSize - defaultModeSlug as Mode, // mode + testModeSlug as Mode, // mode customModePrompts, // customModePrompts undefined, // customModes undefined, // globalCustomInstructions @@ -461,8 +463,8 @@ describe("SYSTEM_PROMPT", () => { false, // enableMcpServerCreation ) - // Should use the default mode's role definition - expect(prompt.indexOf(modes[0].roleDefinition)).toBeLessThan(prompt.indexOf("TOOL USE")) + // Should use the requested mode's role definition + expect(prompt.indexOf(getModeConfig(testModeSlug).roleDefinition)).toBeLessThan(prompt.indexOf("TOOL USE")) }) afterAll(() => { @@ -538,7 +540,7 @@ describe("addCustomInstructions", () => { mockMcpHub, // mcpHub undefined, // diffStrategy undefined, // browserViewportSize - defaultModeSlug, // mode + testModeSlug, // mode undefined, // customModePrompts undefined, // customModes, undefined, // globalCustomInstructions @@ -561,7 +563,7 @@ describe("addCustomInstructions", () => { mockMcpHub, // mcpHub undefined, // diffStrategy undefined, // browserViewportSize - defaultModeSlug, // mode + testModeSlug, // mode undefined, // customModePrompts undefined, // customModes, undefined, // globalCustomInstructions @@ -575,17 +577,17 @@ describe("addCustomInstructions", () => { }) it("should prioritize mode-specific rules for code mode", async () => { - const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug) + const instructions = await addCustomInstructions("", "", "/test/path", testModeSlug) expect(instructions).toMatchSnapshot() }) it("should prioritize mode-specific rules for ask mode", async () => { - const instructions = await addCustomInstructions("", "", "/test/path", modes[2].slug) + const instructions = await addCustomInstructions("", "", "/test/path", "ask") expect(instructions).toMatchSnapshot() }) it("should prioritize mode-specific rules for architect mode", async () => { - const instructions = await addCustomInstructions("", "", "/test/path", modes[1].slug) + const instructions = await addCustomInstructions("", "", "/test/path", "architect") expect(instructions).toMatchSnapshot() }) @@ -600,50 +602,41 @@ describe("addCustomInstructions", () => { }) it("should fall back to generic rules when mode-specific rules not found", async () => { - const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug) + const instructions = await addCustomInstructions("", "", "/test/path", testModeSlug) expect(instructions).toMatchSnapshot() }) it("should include preferred language when provided", async () => { - const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug, { + const instructions = await addCustomInstructions("", "", "/test/path", testModeSlug, { language: "es", }) expect(instructions).toMatchSnapshot() }) it("should include custom instructions when provided", async () => { - const instructions = await addCustomInstructions("Custom test instructions", "", "/test/path", defaultModeSlug) + const instructions = await addCustomInstructions("Custom test instructions", "", "/test/path", testModeSlug) expect(instructions).toMatchSnapshot() }) it("should combine all custom instructions", async () => { - const instructions = await addCustomInstructions( - "Custom test instructions", - "", - "/test/path", - defaultModeSlug, - { language: "fr" }, - ) + const instructions = await addCustomInstructions("Custom test instructions", "", "/test/path", testModeSlug, { + language: "fr", + }) expect(instructions).toMatchSnapshot() }) it("should handle undefined mode-specific instructions", async () => { - const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug) + const instructions = await addCustomInstructions("", "", "/test/path", testModeSlug) expect(instructions).toMatchSnapshot() }) it("should trim mode-specific instructions", async () => { - const instructions = await addCustomInstructions( - " Custom mode instructions ", - "", - "/test/path", - defaultModeSlug, - ) + const instructions = await addCustomInstructions(" Custom mode instructions ", "", "/test/path", testModeSlug) expect(instructions).toMatchSnapshot() }) it("should handle empty mode-specific instructions", async () => { - const instructions = await addCustomInstructions("", "", "/test/path", defaultModeSlug) + const instructions = await addCustomInstructions("", "", "/test/path", testModeSlug) expect(instructions).toMatchSnapshot() }) @@ -652,7 +645,7 @@ describe("addCustomInstructions", () => { "Mode-specific instructions", "Global instructions", "/test/path", - defaultModeSlug, + testModeSlug, ) expect(instructions).toMatchSnapshot() }) @@ -662,7 +655,7 @@ describe("addCustomInstructions", () => { "Second instruction", "First instruction", "/test/path", - defaultModeSlug, + testModeSlug, ) const instructionParts = instructions.split("\n\n") diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 14a5afb6616..de6ff082043 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -50,7 +50,7 @@ import { telemetryService } from "../../services/telemetry/TelemetryService" import { getWorkspacePath } from "../../utils/path" import { webviewMessageHandler } from "./webviewMessageHandler" import { WebviewMessage } from "../../shared/WebviewMessage" -import { PEARAI_URL } from "../../shared/pearaiApi" +import { PEARAI_URL, pearaiDefaultModelId, pearaiModels } from "../../shared/pearaiApi" import { PearAIAgentModelsConfig } from "../../api/providers/pearai/pearai" /** @@ -452,12 +452,20 @@ export class ClineProvider extends EventEmitter implements } public async getPearAIAgentModels() { - const response = await fetch(`${PEARAI_URL}/getPearAIAgentModels`) - if (!response.ok) { - throw new Error(`Failed to fetch models: ${response.statusText}`) + try { + const response = await fetch(`${PEARAI_URL}/getPearAIAgentModels`) + if (!response.ok) { + throw new Error(`Failed to fetch models: ${response.statusText}`) + } + const data = (await response.json()) as PearAIAgentModelsConfig + return data + } catch (error) { + console.warn("Falling back to bundled PearAI models:", error) + return { + models: pearaiModels, + defaultModelId: pearaiDefaultModelId, + } satisfies PearAIAgentModelsConfig } - const data = (await response.json()) as PearAIAgentModelsConfig - return data } public async initClineWithSubTask(parent: Cline, task?: string, images?: string[]) { @@ -542,7 +550,7 @@ export class ClineProvider extends EventEmitter implements public async initClineWithHistoryItem( historyItem: HistoryItem & { rootTask?: Cline; parentTask?: Cline }, - options?: { creatorModeConfig?: CreatorModeConfig } + options?: { creatorModeConfig?: CreatorModeConfig }, ) { await this.removeClineFromStack() @@ -828,7 +836,7 @@ export class ClineProvider extends EventEmitter implements let apiConfig = await this.providerSettingsManager.loadConfig(config.name) // Switch to pearai-model-creator model if we are in Creator Mode - if (newMode == PEARAI_CREATOR_MODE_WEBAPP_MANAGER_SLUG || newMode.includes('creator')) { + if (newMode == PEARAI_CREATOR_MODE_WEBAPP_MANAGER_SLUG || newMode.includes("creator")) { apiConfig = { ...apiConfig, apiProvider: "pearai", @@ -868,7 +876,6 @@ export class ClineProvider extends EventEmitter implements creatorModeConfig: currentCline?.creatorModeConfig, } - if (mode) { const currentApiConfigName = this.getGlobalState("currentApiConfigName") const listApiConfig = await this.providerSettingsManager.listConfig() @@ -1259,12 +1266,12 @@ export class ClineProvider extends EventEmitter implements historyPreviewCollapsed, } = await this.getState() - const creatorModeConfig = currentCline?.creatorModeConfig; + const creatorModeConfig = currentCline?.creatorModeConfig const apiConfiguration = { - ...baseApiConfiguration + ...baseApiConfiguration, } - const telemetryKey = 'phc_EixCfQZYA5It6ZjtZG2C8THsUQzPzXZsdCsvR8AYhfh' + const telemetryKey = "phc_EixCfQZYA5It6ZjtZG2C8THsUQzPzXZsdCsvR8AYhfh" const machineId = vscode.env.machineId const allowedCommands = vscode.workspace.getConfiguration("roo-cline").get("allowedCommands") || [] const cwd = this.cwd diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index 344c9672186..d891f288aef 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -372,10 +372,11 @@ describe("ClineProvider", () => { expect(mockWebviewView.webview.html).toContain("") - // Verify Content Security Policy contains the necessary PostHog domains + // Verify Content Security Policy contains the necessary connection domains. expect(mockWebviewView.webview.html).toContain( - "connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com;", + "connect-src https://openrouter.ai https://api.requesty.ai https://us.i.posthog.com https://us-assets.i.posthog.com", ) + expect(mockWebviewView.webview.html).toContain("https://server.trypear.ai;") // Extract the script-src directive section and verify required security elements const html = mockWebviewView.webview.html @@ -593,12 +594,12 @@ describe("ClineProvider", () => { expect(state.requestDelaySeconds).toBe(10) }) - test("alwaysApproveResubmit defaults to false", async () => { + test("alwaysApproveResubmit defaults to true", async () => { // Mock globalState.get to return undefined for alwaysApproveResubmit ;(mockContext.globalState.get as jest.Mock).mockReturnValue(undefined) const state = await provider.getState() - expect(state.alwaysApproveResubmit).toBe(false) + expect(state.alwaysApproveResubmit).toBe(true) }) test("loads saved API config when switching modes", async () => { @@ -842,20 +843,22 @@ describe("ClineProvider", () => { await provider.initClineWithTask("Test task") // Verify Cline was initialized with mode-specific instructions - expect(Cline).toHaveBeenCalledWith({ - provider, - apiConfiguration: mockApiConfig, - customInstructions: modeCustomInstructions, - enableDiff: true, - enableCheckpoints: false, - fuzzyMatchThreshold: 1.0, - task: "Test task", - experiments: experimentDefault, - rootTask: undefined, - parentTask: undefined, - taskNumber: 1, - onCreated: expect.any(Function), - }) + expect(Cline).toHaveBeenCalledWith( + expect.objectContaining({ + provider, + apiConfiguration: expect.objectContaining(mockApiConfig), + customInstructions: modeCustomInstructions, + enableDiff: true, + enableCheckpoints: false, + fuzzyMatchThreshold: 1.0, + task: "Test task", + experiments: experimentDefault, + rootTask: undefined, + parentTask: undefined, + taskNumber: 1, + onCreated: expect.any(Function), + }), + ) }) test("handles mode-specific custom instructions updates", async () => { diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index d3a05f43ced..44ece7f32ce 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -68,7 +68,7 @@ type ProviderSettings = { cacheWritesPrice?: number | undefined cacheReadsPrice?: number | undefined description?: string | undefined - reasoningEffort?: ("low" | "medium" | "high") | undefined + reasoningEffort?: ("none" | "low" | "medium" | "high" | "xhigh") | undefined thinking?: boolean | undefined minTokensPerCachePoint?: number | undefined maxCachePoints?: number | undefined @@ -125,7 +125,7 @@ type ProviderSettings = { modelMaxTokens?: number | undefined modelMaxThinkingTokens?: number | undefined includeMaxTokens?: boolean | undefined - reasoningEffort?: ("low" | "medium" | "high") | undefined + reasoningEffort?: ("none" | "low" | "medium" | "high" | "xhigh") | undefined promptCachingEnabled?: boolean | undefined diffEnabled?: boolean | undefined fuzzyMatchThreshold?: number | undefined @@ -149,7 +149,7 @@ type ProviderSettings = { cacheWritesPrice?: number | undefined cacheReadsPrice?: number | undefined description?: string | undefined - reasoningEffort?: ("low" | "medium" | "high") | undefined + reasoningEffort?: ("none" | "low" | "medium" | "high" | "xhigh") | undefined thinking?: boolean | undefined minTokensPerCachePoint?: number | undefined maxCachePoints?: number | undefined @@ -182,7 +182,7 @@ type ProviderSettings = { cacheWritesPrice?: number | undefined cacheReadsPrice?: number | undefined description?: string | undefined - reasoningEffort?: ("low" | "medium" | "high") | undefined + reasoningEffort?: ("none" | "low" | "medium" | "high" | "xhigh") | undefined thinking?: boolean | undefined minTokensPerCachePoint?: number | undefined maxCachePoints?: number | undefined diff --git a/src/exports/types.ts b/src/exports/types.ts index e964fdbe46c..67ed072eef9 100644 --- a/src/exports/types.ts +++ b/src/exports/types.ts @@ -69,7 +69,7 @@ type ProviderSettings = { cacheWritesPrice?: number | undefined cacheReadsPrice?: number | undefined description?: string | undefined - reasoningEffort?: ("low" | "medium" | "high") | undefined + reasoningEffort?: ("none" | "low" | "medium" | "high" | "xhigh") | undefined thinking?: boolean | undefined minTokensPerCachePoint?: number | undefined maxCachePoints?: number | undefined @@ -126,7 +126,7 @@ type ProviderSettings = { modelMaxTokens?: number | undefined modelMaxThinkingTokens?: number | undefined includeMaxTokens?: boolean | undefined - reasoningEffort?: ("low" | "medium" | "high") | undefined + reasoningEffort?: ("none" | "low" | "medium" | "high" | "xhigh") | undefined promptCachingEnabled?: boolean | undefined diffEnabled?: boolean | undefined fuzzyMatchThreshold?: number | undefined @@ -150,7 +150,7 @@ type ProviderSettings = { cacheWritesPrice?: number | undefined cacheReadsPrice?: number | undefined description?: string | undefined - reasoningEffort?: ("low" | "medium" | "high") | undefined + reasoningEffort?: ("none" | "low" | "medium" | "high" | "xhigh") | undefined thinking?: boolean | undefined minTokensPerCachePoint?: number | undefined maxCachePoints?: number | undefined @@ -183,7 +183,7 @@ type ProviderSettings = { cacheWritesPrice?: number | undefined cacheReadsPrice?: number | undefined description?: string | undefined - reasoningEffort?: ("low" | "medium" | "high") | undefined + reasoningEffort?: ("none" | "low" | "medium" | "high" | "xhigh") | undefined thinking?: boolean | undefined minTokensPerCachePoint?: number | undefined maxCachePoints?: number | undefined diff --git a/src/integrations/editor/DecorationController.ts b/src/integrations/editor/DecorationController.ts index 8f475408d4d..0d126b5cf98 100644 --- a/src/integrations/editor/DecorationController.ts +++ b/src/integrations/editor/DecorationController.ts @@ -1,12 +1,18 @@ import * as vscode from "vscode" -const fadedOverlayDecorationType = vscode.window.createTextEditorDecorationType({ +const createTextEditorDecorationType = (options: vscode.DecorationRenderOptions) => + vscode.window?.createTextEditorDecorationType?.(options) ?? + ({ + dispose: () => {}, + } as vscode.TextEditorDecorationType) + +const fadedOverlayDecorationType = createTextEditorDecorationType({ backgroundColor: "rgba(255, 255, 0, 0.1)", opacity: "0.4", isWholeLine: true, }) -const activeLineDecorationType = vscode.window.createTextEditorDecorationType({ +const activeLineDecorationType = createTextEditorDecorationType({ backgroundColor: "rgba(255, 255, 0, 0.3)", opacity: "1", isWholeLine: true, diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 9b8175053b8..e0576d09e66 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -89,7 +89,7 @@ export type TelemetrySetting = z.infer * ReasoningEffort */ -export const reasoningEfforts = ["low", "medium", "high"] as const +export const reasoningEfforts = ["none", "low", "medium", "high", "xhigh"] as const export const reasoningEffortsSchema = z.enum(reasoningEfforts) @@ -215,7 +215,7 @@ export const modeConfigSchema = z.object({ customInstructions: z.string().optional(), groups: groupEntryArraySchema, source: z.enum(["global", "project"]).optional(), - backendOnly: z.boolean().optional() + backendOnly: z.boolean().optional(), }) export type ModeConfig = z.infer @@ -328,7 +328,7 @@ export const creatorModeConfigSchema = z.object({ creatorMode: z.boolean().optional(), newProjectType: z.string().optional(), newProjectPath: z.string().optional(), -}); +}) export type CreatorModeConfig = z.infer diff --git a/src/services/mcp/__tests__/McpHub.test.ts b/src/services/mcp/__tests__/McpHub.test.ts index ffd98ff6bda..da585f3dbbc 100644 --- a/src/services/mcp/__tests__/McpHub.test.ts +++ b/src/services/mcp/__tests__/McpHub.test.ts @@ -38,6 +38,38 @@ describe("McpHub", () => { // Store original console methods const originalConsoleError = console.error + const registerTestServer = (source: "global" | "project" = "global") => { + mcpHub.connections = [ + { + server: { + name: "test-server", + config: JSON.stringify({ + type: "stdio", + command: "node", + args: ["test.js"], + alwaysAllow: ["allowed-tool"], + }), + status: "disconnected", + disabled: false, + source, + }, + client: { + request: jest.fn().mockResolvedValue({ content: [] }), + } as any, + transport: {} as any, + }, + ] + } + + const getWrittenConfigs = () => + (fs.writeFile as jest.Mock).mock.calls.flatMap((call) => { + try { + return [JSON.parse(call[1])] + } catch { + return [] + } + }) + beforeEach(() => { jest.clearAllMocks() @@ -104,6 +136,8 @@ describe("McpHub", () => { }, }), ) + ;(fs.access as jest.Mock).mockResolvedValue(undefined) + ;(fs.writeFile as jest.Mock).mockResolvedValue(undefined) mcpHub = new McpHub(mockProvider as ClineProvider) }) @@ -128,6 +162,7 @@ describe("McpHub", () => { // Mock reading initial config ;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig)) + registerTestServer() await mcpHub.toggleToolAlwaysAllow("test-server", "global", "new-tool", true) @@ -136,16 +171,10 @@ describe("McpHub", () => { expect(writeCalls.length).toBeGreaterThan(0) // Find the write call - const callToUse = writeCalls[writeCalls.length - 1] - expect(callToUse).toBeTruthy() - - // The path might be normalized differently on different platforms, - // so we'll just check that we have a call with valid content - const writtenConfig = JSON.parse(callToUse[1]) - expect(writtenConfig.mcpServers).toBeDefined() - expect(writtenConfig.mcpServers["test-server"]).toBeDefined() - expect(Array.isArray(writtenConfig.mcpServers["test-server"].alwaysAllow)).toBe(true) - expect(writtenConfig.mcpServers["test-server"].alwaysAllow).toContain("new-tool") + const writtenConfig = getWrittenConfigs().find((config) => + config.mcpServers?.["test-server"]?.alwaysAllow?.includes("new-tool"), + ) + expect(writtenConfig).toBeDefined() }) it("should remove tool from always allow list when disabling", async () => { @@ -162,6 +191,7 @@ describe("McpHub", () => { // Mock reading initial config ;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig)) + registerTestServer() await mcpHub.toggleToolAlwaysAllow("test-server", "global", "existing-tool", false) @@ -169,17 +199,12 @@ describe("McpHub", () => { const writeCalls = (fs.writeFile as jest.Mock).mock.calls expect(writeCalls.length).toBeGreaterThan(0) - // Find the write call - const callToUse = writeCalls[writeCalls.length - 1] - expect(callToUse).toBeTruthy() - - // The path might be normalized differently on different platforms, - // so we'll just check that we have a call with valid content - const writtenConfig = JSON.parse(callToUse[1]) - expect(writtenConfig.mcpServers).toBeDefined() - expect(writtenConfig.mcpServers["test-server"]).toBeDefined() - expect(Array.isArray(writtenConfig.mcpServers["test-server"].alwaysAllow)).toBe(true) - expect(writtenConfig.mcpServers["test-server"].alwaysAllow).not.toContain("existing-tool") + const writtenConfig = getWrittenConfigs().find( + (config) => + Array.isArray(config.mcpServers?.["test-server"]?.alwaysAllow) && + !config.mcpServers["test-server"].alwaysAllow.includes("existing-tool"), + ) + expect(writtenConfig).toBeDefined() }) it("should initialize alwaysAllow if it does not exist", async () => { @@ -195,6 +220,7 @@ describe("McpHub", () => { // Mock reading initial config ;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig)) + registerTestServer() await mcpHub.toggleToolAlwaysAllow("test-server", "global", "new-tool", true) @@ -207,7 +233,10 @@ describe("McpHub", () => { const writeCall = writeCalls.find((call) => call[0] === normalizedSettingsPath) const callToUse = writeCall || writeCalls[0] - const writtenConfig = JSON.parse(callToUse[1]) + const writtenConfig = + getWrittenConfigs().find((config) => + config.mcpServers?.["test-server"]?.alwaysAllow?.includes("new-tool"), + ) ?? JSON.parse(callToUse[1]) expect(writtenConfig.mcpServers["test-server"].alwaysAllow).toBeDefined() expect(writtenConfig.mcpServers["test-server"].alwaysAllow).toContain("new-tool") }) @@ -228,6 +257,7 @@ describe("McpHub", () => { // Mock reading initial config ;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig)) + registerTestServer() await mcpHub.toggleServerDisabled("test-server", true) @@ -445,6 +475,7 @@ describe("McpHub", () => { // Mock reading initial config ;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig)) + registerTestServer() await mcpHub.updateServerTimeout("test-server", 120) @@ -475,6 +506,7 @@ describe("McpHub", () => { // Mock initial read ;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig)) + registerTestServer() // Update with invalid timeout await mcpHub.updateServerTimeout("test-server", 3601) @@ -526,6 +558,7 @@ describe("McpHub", () => { } ;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig)) + registerTestServer() // Test valid timeout values const validTimeouts = [1, 60, 3600] @@ -534,6 +567,7 @@ describe("McpHub", () => { expect(fs.writeFile).toHaveBeenCalled() jest.clearAllMocks() // Reset for next iteration ;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig)) + registerTestServer() } }) @@ -550,6 +584,7 @@ describe("McpHub", () => { } ;(fs.readFile as jest.Mock).mockResolvedValueOnce(JSON.stringify(mockConfig)) + registerTestServer() await mcpHub.updateServerTimeout("test-server", 120) diff --git a/src/shared/__tests__/modes.test.ts b/src/shared/__tests__/modes.test.ts index 8882b293574..fdc4360ac3d 100644 --- a/src/shared/__tests__/modes.test.ts +++ b/src/shared/__tests__/modes.test.ts @@ -352,7 +352,6 @@ describe("FileRestrictionError", () => { const result = await getFullModeDetails("non-existent") expect(result).toMatchObject({ ...modes[0], - customInstructions: "", }) }) }) diff --git a/src/shared/api.ts b/src/shared/api.ts index 9cfc1290d8c..6af697ee9bf 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -9,8 +9,57 @@ export type ApiConfiguration = ProviderSettings // Anthropic // https://docs.anthropic.com/en/docs/about-claude/models export type AnthropicModelId = keyof typeof anthropicModels -export const anthropicDefaultModelId: AnthropicModelId = "claude-3-5-sonnet-20241022" +export const anthropicDefaultModelId: AnthropicModelId = "claude-sonnet-4-6" export const anthropicModels = { + "claude-fable-5": { + maxTokens: 128_000, + contextWindow: 1_000_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + inputPrice: 10.0, + outputPrice: 50.0, + cacheWritesPrice: 12.5, + cacheReadsPrice: 1.0, + description: + "Anthropic's most capable widely released model for demanding reasoning and long-horizon agentic work.", + }, + "claude-opus-4-8": { + maxTokens: 128_000, + contextWindow: 1_000_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + inputPrice: 5.0, + outputPrice: 25.0, + cacheWritesPrice: 6.25, + cacheReadsPrice: 0.5, + description: "Opus-tier model for complex reasoning, long-horizon agentic coding, and high-autonomy work.", + }, + "claude-sonnet-4-6": { + maxTokens: 64_000, + contextWindow: 1_000_000, + supportsImages: true, + supportsComputerUse: true, + supportsPromptCache: true, + inputPrice: 3.0, + outputPrice: 15.0, + cacheWritesPrice: 3.75, + cacheReadsPrice: 0.3, + description: "Balanced Claude model for speed, intelligence, coding, and agentic workflows.", + thinking: false, + }, + "claude-haiku-4-5-20251001": { + maxTokens: 64_000, + contextWindow: 200_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 1.0, + outputPrice: 5.0, + cacheWritesPrice: 1.25, + cacheReadsPrice: 0.1, + description: "Fast Claude model with near-frontier intelligence for latency-sensitive workloads.", + }, "claude-3-7-sonnet-20250219:thinking": { maxTokens: 128_000, contextWindow: 200_000, @@ -649,8 +698,56 @@ export const openAiModelInfoSaneDefaults: ModelInfo = { // Gemini // https://ai.google.dev/gemini-api/docs/models/gemini export type GeminiModelId = keyof typeof geminiModels -export const geminiDefaultModelId: GeminiModelId = "gemini-2.0-flash-001" +export const geminiDefaultModelId: GeminiModelId = "gemini-3.5-flash" export const geminiModels = { + "gemini-3.5-flash": { + maxTokens: 65_536, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-3.1-pro-preview": { + maxTokens: 65_536, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-3.1-flash-lite": { + maxTokens: 65_536, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-2.5-pro": { + maxTokens: 65_536, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-2.5-flash": { + maxTokens: 65_536, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0, + outputPrice: 0, + }, + "gemini-2.5-flash-lite": { + maxTokens: 65_536, + contextWindow: 1_048_576, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0, + outputPrice: 0, + }, "gemini-2.5-flash-preview-04-17:thinking": { maxTokens: 65_535, contextWindow: 1_048_576, @@ -832,8 +929,70 @@ export const geminiModels = { // OpenAI Native // https://openai.com/api/pricing/ export type OpenAiNativeModelId = keyof typeof openAiNativeModels -export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-4.1" +export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-5.5" export const openAiNativeModels = { + "gpt-5.5": { + maxTokens: 128_000, + contextWindow: 1_050_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 5, + outputPrice: 30, + cacheWritesPrice: 5, + cacheReadsPrice: 0.5, + reasoningEffort: "medium", + }, + "gpt-5.5-pro": { + maxTokens: 128_000, + contextWindow: 1_050_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 30, + outputPrice: 180, + reasoningEffort: "xhigh", + }, + "gpt-5.4": { + maxTokens: 128_000, + contextWindow: 1_050_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 2.5, + outputPrice: 15, + cacheWritesPrice: 2.5, + cacheReadsPrice: 0.25, + reasoningEffort: "medium", + }, + "gpt-5.4-pro": { + maxTokens: 128_000, + contextWindow: 1_050_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 30, + outputPrice: 180, + reasoningEffort: "xhigh", + }, + "gpt-5.4-mini": { + maxTokens: 128_000, + contextWindow: 400_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0.75, + outputPrice: 4.5, + cacheWritesPrice: 0.75, + cacheReadsPrice: 0.075, + reasoningEffort: "medium", + }, + "gpt-5.4-nano": { + maxTokens: 128_000, + contextWindow: 400_000, + supportsImages: true, + supportsPromptCache: true, + inputPrice: 0.2, + outputPrice: 1.25, + cacheWritesPrice: 0.2, + cacheReadsPrice: 0.02, + reasoningEffort: "low", + }, "gpt-4.1": { maxTokens: 32_768, contextWindow: 1_047_576, @@ -1010,8 +1169,30 @@ export const openAiNativeModels = { // DeepSeek // https://platform.deepseek.com/docs/api export type DeepSeekModelId = keyof typeof deepSeekModels -export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-chat" +export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-v4-flash" export const deepSeekModels = { + "deepseek-v4-flash": { + maxTokens: 384_000, + contextWindow: 1_000_000, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.14, + outputPrice: 0.28, + cacheWritesPrice: 0.14, + cacheReadsPrice: 0.0028, + description: `DeepSeek-V4-Flash is the current fast model. It supports both non-thinking and thinking modes.`, + }, + "deepseek-v4-pro": { + maxTokens: 384_000, + contextWindow: 1_000_000, + supportsImages: false, + supportsPromptCache: true, + inputPrice: 0.435, + outputPrice: 0.87, + cacheWritesPrice: 0.435, + cacheReadsPrice: 0.003625, + description: `DeepSeek-V4-Pro is the current higher-capability model for complex reasoning and coding tasks.`, + }, "deepseek-chat": { maxTokens: 8192, contextWindow: 64_000, @@ -1046,9 +1227,33 @@ export const azureOpenAiDefaultApiVersion = "2024-08-01-preview" export type MistralModelId = keyof typeof mistralModels export const mistralDefaultModelId: MistralModelId = "codestral-latest" export const mistralModels = { - "codestral-latest": { + "mistral-medium-3-5": { + maxTokens: 256_000, + contextWindow: 256_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 1.5, + outputPrice: 7.5, + }, + "mistral-small-2603": { + maxTokens: 256_000, + contextWindow: 256_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0.15, + outputPrice: 0.6, + }, + "mistral-large-2512": { maxTokens: 256_000, contextWindow: 256_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 0.5, + outputPrice: 1.5, + }, + "codestral-latest": { + maxTokens: 128_000, + contextWindow: 128_000, supportsImages: false, supportsPromptCache: false, inputPrice: 0.3, @@ -1111,10 +1316,28 @@ export const unboundDefaultModelInfo: ModelInfo = { } // xAI -// https://docs.x.ai/docs/api-reference +// https://docs.x.ai/developers/models export type XAIModelId = keyof typeof xaiModels -export const xaiDefaultModelId: XAIModelId = "grok-3-beta" +export const xaiDefaultModelId: XAIModelId = "grok-4.3" export const xaiModels = { + "grok-4.3": { + maxTokens: 8192, + contextWindow: 1_000_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 1.25, + outputPrice: 2.5, + description: "xAI's current flagship chat model with configurable reasoning and a 1M context window.", + }, + "grok-build-0.1": { + maxTokens: 8192, + contextWindow: 256_000, + supportsImages: true, + supportsPromptCache: false, + inputPrice: 1.0, + outputPrice: 2.0, + description: "xAI's current fast coding model for agentic coding workflows.", + }, "grok-3-beta": { maxTokens: 8192, contextWindow: 131072, @@ -1122,7 +1345,7 @@ export const xaiModels = { supportsPromptCache: false, inputPrice: 3.0, outputPrice: 15.0, - description: "xAI's Grok-3 beta model with 131K context window", + description: "xAI's legacy Grok-3 beta alias.", }, "grok-3-fast-beta": { maxTokens: 8192, @@ -1131,7 +1354,7 @@ export const xaiModels = { supportsPromptCache: false, inputPrice: 5.0, outputPrice: 25.0, - description: "xAI's Grok-3 fast beta model with 131K context window", + description: "xAI's legacy Grok-3 fast beta alias.", }, "grok-3-mini-beta": { maxTokens: 8192, @@ -1140,7 +1363,7 @@ export const xaiModels = { supportsPromptCache: false, inputPrice: 0.3, outputPrice: 0.5, - description: "xAI's Grok-3 mini beta model with 131K context window", + description: "xAI's legacy Grok-3 mini beta alias.", }, "grok-3-mini-fast-beta": { maxTokens: 8192, @@ -1149,7 +1372,7 @@ export const xaiModels = { supportsPromptCache: false, inputPrice: 0.6, outputPrice: 4.0, - description: "xAI's Grok-3 mini fast beta model with 131K context window", + description: "xAI's legacy Grok-3 mini fast beta alias.", }, "grok-2-latest": { maxTokens: 8192, @@ -1158,7 +1381,7 @@ export const xaiModels = { supportsPromptCache: false, inputPrice: 2.0, outputPrice: 10.0, - description: "xAI's Grok-2 model - latest version with 131K context window", + description: "xAI's legacy Grok-2 latest alias.", }, "grok-2": { maxTokens: 8192, @@ -1167,7 +1390,7 @@ export const xaiModels = { supportsPromptCache: false, inputPrice: 2.0, outputPrice: 10.0, - description: "xAI's Grok-2 model with 131K context window", + description: "xAI's legacy Grok-2 alias.", }, "grok-2-1212": { maxTokens: 8192, @@ -1176,7 +1399,7 @@ export const xaiModels = { supportsPromptCache: false, inputPrice: 2.0, outputPrice: 10.0, - description: "xAI's Grok-2 model (version 1212) with 131K context window", + description: "xAI's legacy Grok-2 1212 alias.", }, "grok-2-vision-latest": { maxTokens: 8192, @@ -1185,7 +1408,7 @@ export const xaiModels = { supportsPromptCache: false, inputPrice: 2.0, outputPrice: 10.0, - description: "xAI's Grok-2 Vision model - latest version with image support and 32K context window", + description: "xAI's legacy Grok-2 Vision latest alias.", }, "grok-2-vision": { maxTokens: 8192, @@ -1194,7 +1417,7 @@ export const xaiModels = { supportsPromptCache: false, inputPrice: 2.0, outputPrice: 10.0, - description: "xAI's Grok-2 Vision model with image support and 32K context window", + description: "xAI's legacy Grok-2 Vision alias.", }, "grok-2-vision-1212": { maxTokens: 8192, @@ -1203,7 +1426,7 @@ export const xaiModels = { supportsPromptCache: false, inputPrice: 2.0, outputPrice: 10.0, - description: "xAI's Grok-2 Vision model (version 1212) with image support and 32K context window", + description: "xAI's legacy Grok-2 Vision 1212 alias.", }, "grok-vision-beta": { maxTokens: 8192, @@ -1212,7 +1435,7 @@ export const xaiModels = { supportsPromptCache: false, inputPrice: 5.0, outputPrice: 15.0, - description: "xAI's Grok Vision Beta model with image support and 8K context window", + description: "xAI's legacy Grok Vision beta alias.", }, "grok-beta": { maxTokens: 8192, @@ -1221,7 +1444,7 @@ export const xaiModels = { supportsPromptCache: false, inputPrice: 5.0, outputPrice: 15.0, - description: "xAI's Grok Beta model (legacy) with 131K context window", + description: "xAI's legacy Grok beta alias.", }, } as const satisfies Record @@ -1413,7 +1636,12 @@ export const vscodeLlmModels = { */ // These models support reasoning efforts. -export const REASONING_MODELS = new Set(["x-ai/grok-3-mini-beta", "grok-3-mini-beta", "grok-3-mini-fast-beta"]) +export const REASONING_MODELS = new Set([ + "grok-4.3", + "x-ai/grok-3-mini-beta", + "grok-3-mini-beta", + "grok-3-mini-fast-beta", +]) // These models support prompt caching. export const PROMPT_CACHING_MODELS = new Set([ diff --git a/src/shared/creatorMode.ts b/src/shared/creatorMode.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/webview-ui/jest.config.cjs b/webview-ui/jest.config.cjs index 2d683ebab3e..3844a288013 100644 --- a/webview-ui/jest.config.cjs +++ b/webview-ui/jest.config.cjs @@ -9,17 +9,18 @@ module.exports = { setupFilesAfterEnv: ["/src/setupTests.ts"], moduleNameMapper: { "\\.(css|less|scss|sass)$": "identity-obj-proxy", + "\\.(svg|png|jpg|jpeg|gif|webp)$": "/src/__mocks__/fileMock.js", "^vscrui$": "/src/__mocks__/vscrui.ts", "^@vscode/webview-ui-toolkit/react$": "/src/__mocks__/@vscode/webview-ui-toolkit/react.ts", "^@/(.*)$": "/src/$1", - '^@roo/(.*)$': '/../src/$1', - '^@src/(.*)$': '/src/$1', + "^@roo/(.*)$": "/../src/$1", + "^@src/(.*)$": "/src/$1", "^src/i18n/setup$": "/src/__mocks__/i18n/setup.ts", "^\\.\\./setup$": "/src/__mocks__/i18n/setup.ts", "^\\./setup$": "/src/__mocks__/i18n/setup.ts", "^src/i18n/TranslationContext$": "/src/__mocks__/i18n/TranslationContext.tsx", "^\\.\\./TranslationContext$": "/src/__mocks__/i18n/TranslationContext.tsx", - "^\\./TranslationContext$": "/src/__mocks__/i18n/TranslationContext.tsx" + "^\\./TranslationContext$": "/src/__mocks__/i18n/TranslationContext.tsx", }, reporters: [["jest-simple-dot-reporter", {}]], transformIgnorePatterns: [ diff --git a/webview-ui/src/__mocks__/fileMock.js b/webview-ui/src/__mocks__/fileMock.js new file mode 100644 index 00000000000..ebf20155e6d --- /dev/null +++ b/webview-ui/src/__mocks__/fileMock.js @@ -0,0 +1 @@ +module.exports = "test-file-stub" diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx index 0f8ad2cbc64..cd2161ccfcd 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx @@ -1,7 +1,6 @@ import { render, fireEvent, screen } from "@testing-library/react" import ChatTextArea from "../ChatTextArea" import { useExtensionState } from "@src/context/ExtensionStateContext" -import { vscode } from "@src/utils/vscode" import { defaultModeSlug } from "@roo/shared/modes" import * as pathMentions from "@src/utils/path-mentions" @@ -25,22 +24,11 @@ jest.mock("@src/utils/path-mentions", () => ({ })) // Get the mocked postMessage function -const mockPostMessage = vscode.postMessage as jest.Mock const mockConvertToMentionPath = pathMentions.convertToMentionPath as jest.Mock // Mock ExtensionStateContext jest.mock("@src/context/ExtensionStateContext") -// Custom query function to get the enhance prompt button -const getEnhancePromptButton = () => { - return screen.getByRole("button", { - name: (_, element) => { - // Find the button with the sparkle icon - return element.querySelector(".codicon-sparkle") !== null - }, - }) -} - describe("ChatTextArea", () => { const defaultProps = { inputValue: "", @@ -71,99 +59,6 @@ describe("ChatTextArea", () => { }) }) - describe("enhance prompt button", () => { - it("should be disabled when textAreaDisabled is true", () => { - ;(useExtensionState as jest.Mock).mockReturnValue({ - filePaths: [], - openedTabs: [], - }) - render() - const enhanceButton = getEnhancePromptButton() - expect(enhanceButton).toHaveClass("cursor-not-allowed") - }) - }) - - describe("handleEnhancePrompt", () => { - it("should send message with correct configuration when clicked", () => { - const apiConfiguration = { - apiProvider: "openrouter", - apiKey: "test-key", - } - - ;(useExtensionState as jest.Mock).mockReturnValue({ - filePaths: [], - openedTabs: [], - apiConfiguration, - }) - - render() - - const enhanceButton = getEnhancePromptButton() - fireEvent.click(enhanceButton) - - expect(mockPostMessage).toHaveBeenCalledWith({ - type: "enhancePrompt", - text: "Test prompt", - }) - }) - - it("should not send message when input is empty", () => { - ;(useExtensionState as jest.Mock).mockReturnValue({ - filePaths: [], - openedTabs: [], - apiConfiguration: { - apiProvider: "openrouter", - }, - }) - - render() - - const enhanceButton = getEnhancePromptButton() - fireEvent.click(enhanceButton) - - expect(mockPostMessage).not.toHaveBeenCalled() - }) - - it("should show loading state while enhancing", () => { - ;(useExtensionState as jest.Mock).mockReturnValue({ - filePaths: [], - openedTabs: [], - apiConfiguration: { - apiProvider: "openrouter", - }, - }) - - render() - - const enhanceButton = getEnhancePromptButton() - fireEvent.click(enhanceButton) - - const loadingSpinner = screen.getByText("", { selector: ".codicon-loading" }) - expect(loadingSpinner).toBeInTheDocument() - }) - }) - - describe("effect dependencies", () => { - it("should update when apiConfiguration changes", () => { - const { rerender } = render() - - // Update apiConfiguration - ;(useExtensionState as jest.Mock).mockReturnValue({ - filePaths: [], - openedTabs: [], - apiConfiguration: { - apiProvider: "openrouter", - newSetting: "test", - }, - }) - - rerender() - - // Verify the enhance button appears after apiConfiguration changes - expect(getEnhancePromptButton()).toBeInTheDocument() - }) - }) - describe("enhanced prompt response", () => { it("should update input value when receiving enhanced prompt", () => { const setInputValue = jest.fn() diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx index 81f3dea1fd6..8cfbcc1b3d3 100644 --- a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx +++ b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx @@ -125,7 +125,7 @@ class MockResizeObserver { global.ResizeObserver = MockResizeObserver -const renderSettingsView = () => { +const renderSettingsView = (state: any = {}) => { const onDone = jest.fn() const queryClient = new QueryClient() @@ -138,7 +138,7 @@ const renderSettingsView = () => { ) // Hydrate initial state. - mockPostMessage({}) + mockPostMessage(state) return { onDone } } @@ -298,33 +298,26 @@ describe("SettingsView - Allowed Commands", () => { jest.clearAllMocks() }) - it("shows allowed commands section when alwaysAllowExecute is enabled", () => { - renderSettingsView() + it("shows allowed commands section when alwaysAllowExecute is enabled", async () => { + renderSettingsView({ alwaysAllowExecute: true }) - // Enable always allow execute - const executeCheckbox = screen.getByTestId("always-allow-execute-toggle") - fireEvent.click(executeCheckbox) // Verify allowed commands section appears - expect(screen.getByTestId("allowed-commands-heading")).toBeInTheDocument() - expect(screen.getByTestId("command-input")).toBeInTheDocument() + expect(await screen.findByTestId("allowed-commands-heading")).toBeInTheDocument() + expect(await screen.findByTestId("command-input")).toBeInTheDocument() }) - it("adds new command to the list", () => { - renderSettingsView() - - // Enable always allow execute - const executeCheckbox = screen.getByTestId("always-allow-execute-toggle") - fireEvent.click(executeCheckbox) + it("adds new command to the list", async () => { + renderSettingsView({ alwaysAllowExecute: true }) // Add a new command - const input = screen.getByTestId("command-input") + const input = await screen.findByTestId("command-input") fireEvent.change(input, { target: { value: "npm test" } }) - const addButton = screen.getByTestId("add-command-button") + const addButton = await screen.findByTestId("add-command-button") fireEvent.click(addButton) // Verify command was added - expect(screen.getByText("npm test")).toBeInTheDocument() + expect(await screen.findByText("npm test")).toBeInTheDocument() // Verify VSCode message was sent expect(vscode.postMessage).toHaveBeenCalledWith({ @@ -333,21 +326,17 @@ describe("SettingsView - Allowed Commands", () => { }) }) - it("removes command from the list", () => { - renderSettingsView() - - // Enable always allow execute - const executeCheckbox = screen.getByTestId("always-allow-execute-toggle") - fireEvent.click(executeCheckbox) + it("removes command from the list", async () => { + renderSettingsView({ alwaysAllowExecute: true }) // Add a command - const input = screen.getByTestId("command-input") + const input = await screen.findByTestId("command-input") fireEvent.change(input, { target: { value: "npm test" } }) - const addButton = screen.getByTestId("add-command-button") + const addButton = await screen.findByTestId("add-command-button") fireEvent.click(addButton) // Remove the command - const removeButton = screen.getByTestId("remove-command-0") + const removeButton = await screen.findByTestId("remove-command-0") fireEvent.click(removeButton) // Verify command was removed @@ -360,16 +349,12 @@ describe("SettingsView - Allowed Commands", () => { }) }) - it("prevents duplicate commands", () => { - renderSettingsView() - - // Enable always allow execute - const executeCheckbox = screen.getByTestId("always-allow-execute-toggle") - fireEvent.click(executeCheckbox) + it("prevents duplicate commands", async () => { + renderSettingsView({ alwaysAllowExecute: true }) // Add a command twice - const input = screen.getByTestId("command-input") - const addButton = screen.getByTestId("add-command-button") + const input = await screen.findByTestId("command-input") + const addButton = await screen.findByTestId("add-command-button") // First addition fireEvent.change(input, { target: { value: "npm test" } }) @@ -384,17 +369,13 @@ describe("SettingsView - Allowed Commands", () => { expect(commands).toHaveLength(1) }) - it("saves allowed commands when clicking Save", () => { - renderSettingsView() - - // Enable always allow execute - const executeCheckbox = screen.getByTestId("always-allow-execute-toggle") - fireEvent.click(executeCheckbox) + it("saves allowed commands when clicking Save", async () => { + renderSettingsView({ alwaysAllowExecute: true }) // Add a command - const input = screen.getByTestId("command-input") + const input = await screen.findByTestId("command-input") fireEvent.change(input, { target: { value: "npm test" } }) - const addButton = screen.getByTestId("add-command-button") + const addButton = await screen.findByTestId("add-command-button") fireEvent.click(addButton) // Click Save diff --git a/webview-ui/src/components/ui/ShortcutsButton.tsx b/webview-ui/src/components/ui/ShortcutsButton.tsx deleted file mode 100644 index 08c4f3474fb..00000000000 --- a/webview-ui/src/components/ui/ShortcutsButton.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import styled from "styled-components" -import { Fragment } from "react" -import { lightGray, vscEditorBackground, vscForeground } from "./index" - -interface ShortcutButtonProps { - keys: string[] - onClick?: () => void - offFocus?: boolean - className?: string - label?: string - labelInside?: boolean // New prop -} - -const StyledShortcutButton = styled.div<{ offFocus: boolean }>` - padding: 1px 4px; - gap: 2px; - display: flex; - align-items: center; - color: ${vscForeground}; - background-color: ${vscEditorBackground}; - border: 1.5px solid ${(props) => (props.offFocus ? undefined : lightGray + "33")}; - border-radius: 6px; -` - -const LabelSpan = styled.span` - opacity: 0.7; - margin-left: 6px; -` - -const KeySpan = styled.span` - font-weight: 500; -` - -const PlusSpan = styled.span` - margin-bottom: 2px; - font-weight: 600; - opacity: 0.5; -` - -export function ShortcutButton({ keys, onClick, offFocus = false, label, labelInside = false }: ShortcutButtonProps) { - return ( -
- - {keys.map((key, index) => ( - - {key} - {index < keys.length - 1 && +} - - ))} - {labelInside && label && {label}} - - {!labelInside && label} -
- ) -} diff --git a/webview-ui/src/components/ui/tail.tsx b/webview-ui/src/components/ui/tail.tsx deleted file mode 100644 index 6f69cf1c254..00000000000 --- a/webview-ui/src/components/ui/tail.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from "react" -import { cn } from "../../lib/utils" -import { vscEditorBackground } from "." - -interface TailProps extends React.HTMLAttributes { - className?: string -} - -const Tail = React.forwardRef(({ className, ...props }, ref) => { - return ( -
- {/* sdfasdf */} - - - -
- ) -}) - -const Tail2 = React.forwardRef(({ className, ...props }, ref) => { - return ( -
- - - -
- ) -}) - -Tail.displayName = "Tail" -Tail2.displayName = "Tail2" - -export { Tail, Tail2 } diff --git a/webview-ui/src/components/welcome/WelcomeView.tsx b/webview-ui/src/components/welcome/WelcomeView.tsx deleted file mode 100644 index 536a654844b..00000000000 --- a/webview-ui/src/components/welcome/WelcomeView.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { useCallback, useState } from "react" -import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react" -import { useExtensionState } from "@src/context/ExtensionStateContext" -import { validateApiConfiguration } from "@src/utils/validate" -import { vscode } from "@src/utils/vscode" -import ApiOptions from "../settings/ApiOptions" -import { Tab, TabContent } from "../common/Tab" -import { Trans } from "react-i18next" -import { useAppTranslation } from "@src/i18n/TranslationContext" -import { getRequestyAuthUrl, getOpenRouterAuthUrl } from "@src/oauth/urls" -import RooHero from "./RooHero" -import knuthShuffle from "knuth-shuffle-seeded" - -const WelcomeView = () => { - const { apiConfiguration, currentApiConfigName, setApiConfiguration, uriScheme, machineId } = useExtensionState() - const { t } = useAppTranslation() - const [errorMessage, setErrorMessage] = useState(undefined) - - const handleSubmit = useCallback(() => { - const error = apiConfiguration ? validateApiConfiguration(apiConfiguration) : undefined - - if (error) { - setErrorMessage(error) - return - } - - setErrorMessage(undefined) - vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) - }, [apiConfiguration, currentApiConfigName]) - - // Using a lazy initializer so it reads once at mount - const [imagesBaseUri] = useState(() => { - const w = window as any - return w.IMAGES_BASE_URI || "" - }) - - return ( - - - -

{t("chat:greeting")}

- -
- -
- -
-

{t("welcome:startRouter")}

- -
- {/* Define the providers */} - {(() => { - // Provider card configuration - const providers = [ - { - slug: "requesty", - name: "Requesty", - description: t("welcome:routers.requesty.description"), - incentive: t("welcome:routers.requesty.incentive"), - authUrl: getRequestyAuthUrl(uriScheme), - }, - { - slug: "openrouter", - name: "OpenRouter", - description: t("welcome:routers.openrouter.description"), - authUrl: getOpenRouterAuthUrl(uriScheme), - }, - ] - - // Shuffle providers based on machine ID (will be consistent for the same machine) - const orderedProviders = [...providers] - knuthShuffle(orderedProviders, (machineId as any) || Date.now()) - - // Render the provider cards - return orderedProviders.map((provider, index) => ( - -
{provider.name}
-
- {provider.name} -
-
-
- {provider.description} -
- {provider.incentive && ( -
{provider.incentive}
- )} -
-
- )) - })()} -
- -
{t("welcome:or")}
-

{t("welcome:startCustom")}

- setApiConfiguration({ [field]: value })} - errorMessage={errorMessage} - setErrorMessage={setErrorMessage} - /> -
-
-
-
-
- { - e.preventDefault() - vscode.postMessage({ type: "importSettings" }) - }} - className="text-sm"> - {t("welcome:importSettings")} - -
- - {t("welcome:start")} - - {errorMessage &&
{errorMessage}
} -
-
-
- ) -} - -export default WelcomeView