Skip to content

Commit f0bd734

Browse files
author
feifei
committed
feat: merge main into feature-branch and resolve merge conflicts
2 parents 008ab8c + 6c371e1 commit f0bd734

File tree

21 files changed

+367
-109
lines changed

21 files changed

+367
-109
lines changed

.changeset/clever-news-arrive.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Roo Code Changelog
22

3+
## [3.3.20]
4+
5+
- Support project-specific custom modes in a .roomodes file
6+
- Add more Mistral models (thanks @d-oit and @bramburn!)
7+
- By popular request, make it so Ask mode can't write to Markdown files and is purely for chatting with
8+
- Add a setting to control the number of open editor tabs to tell the model about (665 is probably too many!)
9+
- Fix race condition bug with entering API key on the welcome screen
10+
311
## [3.3.19]
412

513
- Fix a bug where aborting in the middle of file writes would not revert the write

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline" target="_blank"><img src="https://img.shields.io/badge/Download%20on%20VS%20Marketplace-blue?style=for-the-badge&logo=visualstudiocode&logoColor=white" alt="Download on VS Marketplace"></a>
1616
<a href="https://github.com/RooVetGit/Roo-Code/discussions/categories/feature-requests?discussions_q=is%3Aopen+category%3A%22Feature+Requests%22+sort%3Atop" target="_blank"><img src="https://img.shields.io/badge/Feature%20Requests-yellow?style=for-the-badge" alt="Feature Requests"></a>
1717
<a href="https://marketplace.visualstudio.com/items?itemName=RooVeterinaryInc.roo-cline&ssr=false#review-details" target="_blank"><img src="https://img.shields.io/badge/Rate%20%26%20Review-green?style=for-the-badge" alt="Rate & Review"></a>
18+
<a href="https://docs.roocode.com" target="_blank"><img src="https://img.shields.io/badge/Documentation-6B46C1?style=for-the-badge&logo=readthedocs&logoColor=white" alt="Documentation"></a>
19+
1820
</div>
1921

2022
**Roo Code** is an AI-powered **autonomous coding agent** that lives in your editor. It can:

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"name": "roo-cline",
33
"displayName": "Roo Code (prev. Roo Cline)",
4-
"description": "A VS Code plugin that enhances coding with AI-powered automation, multi-model support, and experimental features.",
4+
"description": "An AI-powered autonomous coding agent that lives in your editor.",
55
"publisher": "RooVeterinaryInc",
6-
"version": "3.3.19",
6+
"version": "3.3.20",
77
"icon": "assets/icons/rocket.png",
88
"galleryBanner": {
99
"color": "#617A91",

src/__mocks__/get-folder-size.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,10 @@ module.exports = async function getFolderSize() {
44
errors: [],
55
}
66
}
7+
8+
module.exports.loose = async function getFolderSizeLoose() {
9+
return {
10+
size: 1000,
11+
errors: [],
12+
}
13+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { MistralHandler } from "../mistral"
2+
import { ApiHandlerOptions, mistralDefaultModelId } from "../../../shared/api"
3+
import { Anthropic } from "@anthropic-ai/sdk"
4+
import { ApiStreamTextChunk } from "../../transform/stream"
5+
6+
// Mock Mistral client
7+
const mockCreate = jest.fn()
8+
jest.mock("@mistralai/mistralai", () => {
9+
return {
10+
Mistral: jest.fn().mockImplementation(() => ({
11+
chat: {
12+
stream: mockCreate.mockImplementation(async (options) => {
13+
const stream = {
14+
[Symbol.asyncIterator]: async function* () {
15+
yield {
16+
data: {
17+
choices: [
18+
{
19+
delta: { content: "Test response" },
20+
index: 0,
21+
},
22+
],
23+
},
24+
}
25+
},
26+
}
27+
return stream
28+
}),
29+
},
30+
})),
31+
}
32+
})
33+
34+
describe("MistralHandler", () => {
35+
let handler: MistralHandler
36+
let mockOptions: ApiHandlerOptions
37+
38+
beforeEach(() => {
39+
mockOptions = {
40+
apiModelId: "codestral-latest", // Update to match the actual model ID
41+
mistralApiKey: "test-api-key",
42+
includeMaxTokens: true,
43+
modelTemperature: 0,
44+
}
45+
handler = new MistralHandler(mockOptions)
46+
mockCreate.mockClear()
47+
})
48+
49+
describe("constructor", () => {
50+
it("should initialize with provided options", () => {
51+
expect(handler).toBeInstanceOf(MistralHandler)
52+
expect(handler.getModel().id).toBe(mockOptions.apiModelId)
53+
})
54+
55+
it("should throw error if API key is missing", () => {
56+
expect(() => {
57+
new MistralHandler({
58+
...mockOptions,
59+
mistralApiKey: undefined,
60+
})
61+
}).toThrow("Mistral API key is required")
62+
})
63+
64+
it("should use custom base URL if provided", () => {
65+
const customBaseUrl = "https://custom.mistral.ai/v1"
66+
const handlerWithCustomUrl = new MistralHandler({
67+
...mockOptions,
68+
mistralCodestralUrl: customBaseUrl,
69+
})
70+
expect(handlerWithCustomUrl).toBeInstanceOf(MistralHandler)
71+
})
72+
})
73+
74+
describe("getModel", () => {
75+
it("should return correct model info", () => {
76+
const model = handler.getModel()
77+
expect(model.id).toBe(mockOptions.apiModelId)
78+
expect(model.info).toBeDefined()
79+
expect(model.info.supportsPromptCache).toBe(false)
80+
})
81+
})
82+
83+
describe("createMessage", () => {
84+
const systemPrompt = "You are a helpful assistant."
85+
const messages: Anthropic.Messages.MessageParam[] = [
86+
{
87+
role: "user",
88+
content: [{ type: "text", text: "Hello!" }],
89+
},
90+
]
91+
92+
it("should create message successfully", async () => {
93+
const iterator = handler.createMessage(systemPrompt, messages)
94+
const result = await iterator.next()
95+
96+
expect(mockCreate).toHaveBeenCalledWith({
97+
model: mockOptions.apiModelId,
98+
messages: expect.any(Array),
99+
maxTokens: expect.any(Number),
100+
temperature: 0,
101+
})
102+
103+
expect(result.value).toBeDefined()
104+
expect(result.done).toBe(false)
105+
})
106+
107+
it("should handle streaming response correctly", async () => {
108+
const iterator = handler.createMessage(systemPrompt, messages)
109+
const results: ApiStreamTextChunk[] = []
110+
111+
for await (const chunk of iterator) {
112+
if ("text" in chunk) {
113+
results.push(chunk as ApiStreamTextChunk)
114+
}
115+
}
116+
117+
expect(results.length).toBeGreaterThan(0)
118+
expect(results[0].text).toBe("Test response")
119+
})
120+
121+
it("should handle errors gracefully", async () => {
122+
mockCreate.mockRejectedValueOnce(new Error("API Error"))
123+
await expect(handler.createMessage(systemPrompt, messages).next()).rejects.toThrow("API Error")
124+
})
125+
})
126+
})

src/api/providers/mistral.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,23 +21,36 @@ export class MistralHandler implements ApiHandler {
2121
private client: Mistral
2222

2323
constructor(options: ApiHandlerOptions) {
24+
if (!options.mistralApiKey) {
25+
throw new Error("Mistral API key is required")
26+
}
27+
2428
this.options = options
29+
const baseUrl = this.getBaseUrl()
30+
console.debug(`[Roo Code] MistralHandler using baseUrl: ${baseUrl}`)
2531
this.client = new Mistral({
26-
serverURL: "https://codestral.mistral.ai",
32+
serverURL: baseUrl,
2733
apiKey: this.options.mistralApiKey,
2834
})
2935
}
3036

37+
private getBaseUrl(): string {
38+
const modelId = this.options.apiModelId
39+
if (modelId?.startsWith("codestral-")) {
40+
return this.options.mistralCodestralUrl || "https://codestral.mistral.ai"
41+
}
42+
return "https://api.mistral.ai"
43+
}
44+
3145
async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
32-
const stream = await this.client.chat.stream({
33-
model: this.getModel().id,
34-
// max_completion_tokens: this.getModel().info.maxTokens,
46+
const response = await this.client.chat.stream({
47+
model: this.options.apiModelId || mistralDefaultModelId,
48+
messages: convertToMistralMessages(messages),
49+
maxTokens: this.options.includeMaxTokens ? this.getModel().info.maxTokens : undefined,
3550
temperature: this.options.modelTemperature ?? MISTRAL_DEFAULT_TEMPERATURE,
36-
messages: [{ role: "system", content: systemPrompt }, ...convertToMistralMessages(messages)],
37-
stream: true,
3851
})
3952

40-
for await (const chunk of stream) {
53+
for await (const chunk of response) {
4154
const delta = chunk.data.choices[0]?.delta
4255
if (delta?.content) {
4356
let content: string = ""

src/core/config/CustomModesManager.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,6 @@ export class CustomModesManager {
6262
const settings = JSON.parse(content)
6363
const result = CustomModesSettingsSchema.safeParse(settings)
6464
if (!result.success) {
65-
const errorMsg = `Schema validation failed for ${filePath}`
66-
console.error(`[CustomModesManager] ${errorMsg}:`, result.error)
6765
return []
6866
}
6967

src/core/config/CustomModesSchema.ts

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,22 +29,19 @@ const GroupOptionsSchema = z.object({
2929
const GroupEntrySchema = z.union([ToolGroupSchema, z.tuple([ToolGroupSchema, GroupOptionsSchema])])
3030

3131
// Schema for array of groups
32-
const GroupsArraySchema = z
33-
.array(GroupEntrySchema)
34-
.min(1, "At least one tool group is required")
35-
.refine(
36-
(groups) => {
37-
const seen = new Set()
38-
return groups.every((group) => {
39-
// For tuples, check the group name (first element)
40-
const groupName = Array.isArray(group) ? group[0] : group
41-
if (seen.has(groupName)) return false
42-
seen.add(groupName)
43-
return true
44-
})
45-
},
46-
{ message: "Duplicate groups are not allowed" },
47-
)
32+
const GroupsArraySchema = z.array(GroupEntrySchema).refine(
33+
(groups) => {
34+
const seen = new Set()
35+
return groups.every((group) => {
36+
// For tuples, check the group name (first element)
37+
const groupName = Array.isArray(group) ? group[0] : group
38+
if (seen.has(groupName)) return false
39+
seen.add(groupName)
40+
return true
41+
})
42+
},
43+
{ message: "Duplicate groups are not allowed" },
44+
)
4845

4946
// Schema for mode configuration
5047
export const CustomModeSchema = z.object({

0 commit comments

Comments
 (0)