Skip to content

Commit 53065c4

Browse files
Merge branch 'roo-newrelease' into upstream-pull-13022025
2 parents 5f0047b + 071165c commit 53065c4

32 files changed

+1350
-557
lines changed

.clinerules

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
- Logs can be found in `logs\app.log`
1717
- Logfile is overwritten on each run to keep it to a manageable volume.
1818

19+
4. Styling Guidelines:
20+
- Use Tailwind CSS classes instead of inline style objects for new markup
21+
- VSCode CSS variables must be added to webview-ui/src/index.css before using them in Tailwind classes
22+
- Example: `<div className="text-md text-vscode-descriptionForeground mb-2" />` instead of style objects
23+
1924

2025
# Adding a New Setting
2126

CHANGELOG.md

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

3+
## 3.3.20
4+
5+
### Patch Changes
6+
7+
- Disable writing in ask mode
8+
- v3.3.20
9+
310
## [3.3.19]
411

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

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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "PearAI Roo Code / Cline",
44
"description": "PearAI's integration of Roo Code / Cline, a coding agent.",
55
"publisher": "PearAI",
6-
"version": "3.3.19",
6+
"version": "3.3.20",
77
"icon": "assets/icons/pear.png",
88
"galleryBanner": {
99
"color": "#617A91",
@@ -287,7 +287,9 @@
287287
"compile:integration": "tsc -p tsconfig.integration.json",
288288
"install:all": "npm install && cd webview-ui && npm install",
289289
"lint": "eslint src --ext ts && npm run lint --prefix webview-ui",
290+
"lint-local": "eslint -c .eslintrc.local.json src --ext ts && npm run lint --prefix webview-ui",
290291
"lint-fix": "eslint src --ext ts --fix && npm run lint-fix --prefix webview-ui",
292+
"lint-fix-local": "eslint -c .eslintrc.local.json src --ext ts --fix && npm run lint-fix --prefix webview-ui",
291293
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
292294
"pretest": "npm run compile && npm run compile:integration",
293295
"dev": "cd webview-ui && npm run dev",

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/Cline.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3112,11 +3112,14 @@ export class Cline {
31123112
}
31133113

31143114
details += "\n\n# VSCode Open Tabs"
3115+
const { maxOpenTabsContext } = (await this.providerRef.deref()?.getState()) ?? {}
3116+
const maxTabs = maxOpenTabsContext ?? 20
31153117
const openTabs = vscode.window.tabGroups.all
31163118
.flatMap((group) => group.tabs)
31173119
.map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
31183120
.filter(Boolean)
31193121
.map((absolutePath) => path.relative(cwd, absolutePath).toPosix())
3122+
.slice(0, maxTabs)
31203123
.join("\n")
31213124
if (openTabs) {
31223125
details += `\n${openTabs}`

src/core/__tests__/Cline.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ describe("Cline", () => {
475475
// Mock abort state
476476
Object.defineProperty(cline, "abort", {
477477
get: () => false,
478+
set: () => {},
478479
configurable: true,
479480
})
480481

@@ -603,10 +604,12 @@ describe("Cline", () => {
603604
// Mock abort state for both instances
604605
Object.defineProperty(clineWithImages, "abort", {
605606
get: () => false,
607+
set: () => {},
606608
configurable: true,
607609
})
608610
Object.defineProperty(clineWithoutImages, "abort", {
609611
get: () => false,
612+
set: () => {},
610613
configurable: true,
611614
})
612615

0 commit comments

Comments
 (0)