Merge branch 'main' into will/mode-plumbing

This commit is contained in:
Will Li 2025-08-24 16:59:29 -07:00
commit bfd67cc935
183 changed files with 3806 additions and 673 deletions

5
.gitattributes vendored
View file

@ -6,6 +6,11 @@ src/assets/docs/demo.gif filter=lfs diff=lfs merge=lfs -text
*.snap linguist-generated=true
# Non-English translation files - mark as linguist-generated to exclude from GitHub language statistics
# Package NLS files - mark non-English ones as generated
src/package.nls.*.json linguist-generated=true
# Exclude the base English file from being marked as generated
src/package.nls.json linguist-generated=false
# Root locales directory (contains only non-English translations)
locales/** linguist-generated=true

View file

@ -1,5 +1,31 @@
# Roo Code Changelog
## [3.25.23] - 2025-08-22
- feat: add custom base URL support for Requesty provider (thanks @requesty-JohnCosta27!)
- feat: add DeepSeek V3.1 model to Chutes AI provider (#7294 by @dmarkey, PR by @app/roomote)
- Revert "feat: enable loading Roo modes from multiple files in .roo/modes directory" temporarily to fix a bug with mode installation
## [3.25.22] - 2025-08-22
- Add prompt caching support for Kimi K2 on Groq (thanks @daniel-lxs and @benank!)
- Add documentation links for global custom instructions in UI (thanks @app/roomote!)
## [3.25.21] - 2025-08-21
- Ensure subtask results are provided to GPT-5 in OpenAI Responses API
- Promote the experimental AssistantMessageParser to the default parser
- Update DeepSeek models context window to 128k (thanks @JuanPerezReal)
- Enable grounding features for Vertex AI (thanks @anguslees)
- Allow orchestrator to pass TODO lists to subtasks
- Improved MDM handling
- Handle nullish token values in ContextCondenseRow to prevent UI crash (thanks @s97712)
- Improved context window error handling for OpenAI and other providers
- Add "installed" filter to Roo Marketplace (thanks @semidark)
- Improve filesystem access checks (thanks @elianiva)
- Support for loading Roo modes from multiple YAML files in the `.roo/modes/` directory (thanks @farazoman)
- Add Featherless provider (thanks @DarinVerheijke)
## [3.25.20] - 2025-08-19
- Add announcement for Sonic model

View file

@ -1 +1 @@
DATABASE_URL=postgres://postgres:password@localhost:5432/evals_development
DATABASE_URL=postgres://postgres:password@localhost:5433/evals_development

View file

@ -5,13 +5,13 @@ if ! docker info &> /dev/null; then
exit 1
fi
if ! nc -z localhost 5432 2>/dev/null; then
if ! nc -z postgres 5433 2>/dev/null; then
echo "❌ PostgreSQL is not running on port 5432"
echo "💡 Start it with: pnpm --filter @roo-code/evals db:up"
exit 1
fi
if ! nc -z localhost 6379 2>/dev/null; then
if ! nc -z redis 6380 2>/dev/null; then
echo "❌ Redis is not running on port 6379"
echo "💡 Start it with: pnpm --filter @roo-code/evals redis:up"
exit 1

View file

@ -8,7 +8,7 @@ import { useForm, FormProvider } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import fuzzysort from "fuzzysort"
import { toast } from "sonner"
import { X, Rocket, Check, ChevronsUpDown, SlidersHorizontal, Book, CircleCheck } from "lucide-react"
import { X, Rocket, Check, ChevronsUpDown, SlidersHorizontal, CircleCheck } from "lucide-react"
import { globalSettingsSchema, providerSettingsSchema, EVALS_SETTINGS, getModelId } from "@roo-code/types"
@ -49,11 +49,8 @@ import {
PopoverContent,
PopoverTrigger,
ScrollArea,
ScrollBar,
Slider,
Dialog,
DialogContent,
DialogTitle,
DialogFooter,
} from "@/components/ui"
import { SettingsDiff } from "./settings-diff"
@ -93,10 +90,6 @@ export function NewRun() {
const [model, suite, settings] = watch(["model", "suite", "settings", "concurrency"])
const [systemPromptDialogOpen, setSystemPromptDialogOpen] = useState(false)
const [systemPrompt, setSystemPrompt] = useState("")
const systemPromptRef = useRef<HTMLTextAreaElement>(null)
const onSubmit = useCallback(
async (values: CreateRun) => {
try {
@ -104,13 +97,13 @@ export function NewRun() {
values.settings = { ...(values.settings || {}), openRouterModelId: model }
}
const { id } = await createRun({ ...values, systemPrompt })
const { id } = await createRun(values)
router.push(`/runs/${id}`)
} catch (e) {
toast.error(e instanceof Error ? e.message : "An unknown error occurred.")
}
},
[mode, model, router, systemPrompt],
[mode, model, router],
)
const onFilterModels = useCallback(
@ -269,29 +262,11 @@ export function NewRun() {
</div>
<SettingsDiff defaultSettings={EVALS_SETTINGS} customSettings={settings} />
</>
<ScrollBar orientation="horizontal" />
</ScrollArea>
)}
<FormMessage />
</FormItem>
<Button type="button" variant="secondary" onClick={() => setSystemPromptDialogOpen(true)}>
<Book />
Override System Prompt
</Button>
<Dialog open={systemPromptDialogOpen} onOpenChange={setSystemPromptDialogOpen}>
<DialogContent>
<DialogTitle>Override System Prompt</DialogTitle>
<Textarea
ref={systemPromptRef}
value={systemPrompt}
onChange={(e) => setSystemPrompt(e.target.value)}
/>
<DialogFooter>
<Button onClick={() => setSystemPromptDialogOpen(false)}>Done</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<FormField

View file

@ -52,13 +52,13 @@ type SettingDiffProps = HTMLAttributes<HTMLDivElement> & {
export function SettingDiff({ name, defaultValue, customValue, ...props }: SettingDiffProps) {
return (
<Fragment {...props}>
<div className="overflow-hidden font-mono" title={name}>
<div className="font-mono" title={name}>
{name}
</div>
<pre className="overflow-hidden inline text-rose-500 line-through" title={defaultValue}>
<pre className="inline text-rose-500 line-through" title={defaultValue}>
{defaultValue}
</pre>
<pre className="overflow-hidden inline text-teal-500" title={customValue}>
<pre className="inline text-teal-500" title={customValue}>
{customValue}
</pre>
</Fragment>

View file

@ -6,7 +6,7 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js"
* ExperimentId
*/
export const experimentIds = ["powerSteering", "multiFileApplyDiff", "preventFocusDisruption", "assistantMessageParser"] as const
export const experimentIds = ["powerSteering", "multiFileApplyDiff", "preventFocusDisruption"] as const
export const experimentIdsSchema = z.enum(experimentIds)
@ -20,7 +20,6 @@ export const experimentsSchema = z.object({
powerSteering: z.boolean().optional(),
multiFileApplyDiff: z.boolean().optional(),
preventFocusDisruption: z.boolean().optional(),
assistantMessageParser: z.boolean().optional(),
})
export type Experiments = z.infer<typeof experimentsSchema>

View file

@ -163,6 +163,8 @@ const vertexSchema = apiModelIdProviderModelSchema.extend({
vertexJsonCredentials: z.string().optional(),
vertexProjectId: z.string().optional(),
vertexRegion: z.string().optional(),
enableUrlContext: z.boolean().optional(),
enableGrounding: z.boolean().optional(),
})
const openAiSchema = baseProviderSettingsSchema.extend({

View file

@ -5,6 +5,7 @@ export type ChutesModelId =
| "deepseek-ai/DeepSeek-R1-0528"
| "deepseek-ai/DeepSeek-R1"
| "deepseek-ai/DeepSeek-V3"
| "deepseek-ai/DeepSeek-V3.1"
| "unsloth/Llama-3.3-70B-Instruct"
| "chutesai/Llama-4-Scout-17B-16E-Instruct"
| "unsloth/Mistral-Nemo-Instruct-2407"
@ -60,6 +61,15 @@ export const chutesModels = {
outputPrice: 0,
description: "DeepSeek V3 model.",
},
"deepseek-ai/DeepSeek-V3.1": {
maxTokens: 32768,
contextWindow: 163840,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek V3.1 model.",
},
"unsloth/Llama-3.3-70B-Instruct": {
maxTokens: 32768, // From Groq
contextWindow: 131072, // From Groq

View file

@ -7,8 +7,8 @@ export const deepSeekDefaultModelId: DeepSeekModelId = "deepseek-chat"
export const deepSeekModels = {
"deepseek-chat": {
maxTokens: 8192,
contextWindow: 64_000,
maxTokens: 8192, // 8K max output
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: true,
inputPrice: 0.27, // $0.27 per million tokens (cache miss)
@ -18,15 +18,15 @@ export const deepSeekModels = {
description: `DeepSeek-V3 achieves a significant breakthrough in inference speed over previous models. It tops the leaderboard among open-source models and rivals the most advanced closed-source models globally.`,
},
"deepseek-reasoner": {
maxTokens: 8192,
contextWindow: 64_000,
maxTokens: 65536, // 64K max output for reasoning mode
contextWindow: 128_000,
supportsImages: false,
supportsPromptCache: true,
inputPrice: 0.55, // $0.55 per million tokens (cache miss)
outputPrice: 2.19, // $2.19 per million tokens
cacheWritesPrice: 0.55, // $0.55 per million tokens (cache miss)
cacheReadsPrice: 0.14, // $0.14 per million tokens (cache hit)
description: `DeepSeek-R1 achieves performance comparable to OpenAI-o1 across math, code, and reasoning tasks. Supports Chain of Thought reasoning with up to 32K tokens.`,
description: `DeepSeek-R1 achieves performance comparable to OpenAI-o1 across math, code, and reasoning tasks. Supports Chain of Thought reasoning with up to 64K output tokens.`,
},
} as const satisfies Record<string, ModelInfo>

View file

@ -94,9 +94,10 @@ export const groqModels = {
maxTokens: 16384,
contextWindow: 131072,
supportsImages: false,
supportsPromptCache: false,
supportsPromptCache: true,
inputPrice: 1.0,
outputPrice: 3.0,
cacheReadsPrice: 0.5, // 50% discount for cached input tokens
description: "Moonshot AI Kimi K2 Instruct 1T model, 128K context.",
},
"openai/gpt-oss-120b": {

446
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff

View file

@ -143,6 +143,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
case "io-intelligence":
return new IOIntelligenceHandler(options)
case "roo":
// Never throw exceptions from provider constructors
// The provider-proxy server will handle authentication and return appropriate error codes
return new RooHandler(options)
case "featherless":
return new FeatherlessHandler(options)

View file

@ -163,6 +163,28 @@ describe("ChutesHandler", () => {
expect(model.info).toEqual(expect.objectContaining(chutesModels[testModelId]))
})
it("should return DeepSeek V3.1 model with correct configuration", () => {
const testModelId: ChutesModelId = "deepseek-ai/DeepSeek-V3.1"
const handlerWithModel = new ChutesHandler({
apiModelId: testModelId,
chutesApiKey: "test-chutes-api-key",
})
const model = handlerWithModel.getModel()
expect(model.id).toBe(testModelId)
expect(model.info).toEqual(
expect.objectContaining({
maxTokens: 32768,
contextWindow: 163840,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek V3.1 model.",
temperature: 0.5, // Non-R1 DeepSeek models use default temperature
}),
)
})
it("should return Qwen3-235B-A22B-Instruct-2507 model with correct configuration", () => {
const testModelId: ChutesModelId = "Qwen/Qwen3-235B-A22B-Instruct-2507"
const handlerWithModel = new ChutesHandler({
@ -394,11 +416,11 @@ describe("ChutesHandler", () => {
expect.objectContaining({
model: modelId,
max_tokens: modelInfo.maxTokens,
temperature: 0.5,
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
stream: true,
stream_options: { include_usage: true },
}),
undefined,
)
})

View file

@ -154,12 +154,26 @@ describe("DeepSeekHandler", () => {
const model = handler.getModel()
expect(model.id).toBe(mockOptions.apiModelId)
expect(model.info).toBeDefined()
expect(model.info.maxTokens).toBe(8192)
expect(model.info.contextWindow).toBe(64_000)
expect(model.info.maxTokens).toBe(8192) // deepseek-chat has 8K max
expect(model.info.contextWindow).toBe(128_000)
expect(model.info.supportsImages).toBe(false)
expect(model.info.supportsPromptCache).toBe(true) // Should be true now
})
it("should return correct model info for deepseek-reasoner", () => {
const handlerWithReasoner = new DeepSeekHandler({
...mockOptions,
apiModelId: "deepseek-reasoner",
})
const model = handlerWithReasoner.getModel()
expect(model.id).toBe("deepseek-reasoner")
expect(model.info).toBeDefined()
expect(model.info.maxTokens).toBe(65536) // deepseek-reasoner has 64K max
expect(model.info.contextWindow).toBe(128_000)
expect(model.info.supportsImages).toBe(false)
expect(model.info.supportsPromptCache).toBe(true)
})
it("should return provided model ID with default model info if model does not exist", () => {
const handlerWithInvalidModel = new DeepSeekHandler({
...mockOptions,

View file

@ -352,11 +352,11 @@ describe("FireworksHandler", () => {
expect.objectContaining({
model: modelId,
max_tokens: modelInfo.maxTokens,
temperature: 0.5,
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
stream: true,
stream_options: { include_usage: true },
}),
undefined,
)
})

View file

@ -108,13 +108,63 @@ describe("GroqHandler", () => {
const firstChunk = await stream.next()
expect(firstChunk.done).toBe(false)
expect(firstChunk.value).toEqual({ type: "usage", inputTokens: 10, outputTokens: 20 })
expect(firstChunk.value).toMatchObject({
type: "usage",
inputTokens: 10,
outputTokens: 20,
cacheWriteTokens: 0,
cacheReadTokens: 0,
})
// Check that totalCost is a number (we don't need to test the exact value as that's tested in cost.spec.ts)
expect(typeof firstChunk.value.totalCost).toBe("number")
})
it("createMessage should handle cached tokens in usage data", async () => {
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: vitest
.fn()
.mockResolvedValueOnce({
done: false,
value: {
choices: [{ delta: {} }],
usage: {
prompt_tokens: 100,
completion_tokens: 50,
prompt_tokens_details: {
cached_tokens: 30,
},
},
},
})
.mockResolvedValueOnce({ done: true }),
}),
}
})
const stream = handler.createMessage("system prompt", [])
const firstChunk = await stream.next()
expect(firstChunk.done).toBe(false)
expect(firstChunk.value).toMatchObject({
type: "usage",
inputTokens: 70, // 100 total - 30 cached
outputTokens: 50,
cacheWriteTokens: 0,
cacheReadTokens: 30,
})
expect(typeof firstChunk.value.totalCost).toBe("number")
})
it("createMessage should pass correct parameters to Groq client", async () => {
const modelId: GroqModelId = "llama-3.1-8b-instant"
const modelInfo = groqModels[modelId]
const handlerWithModel = new GroqHandler({ apiModelId: modelId, groqApiKey: "test-groq-api-key" })
const handlerWithModel = new GroqHandler({
apiModelId: modelId,
groqApiKey: "test-groq-api-key",
modelTemperature: 0.5, // Explicitly set temperature for this test
})
mockCreate.mockImplementationOnce(() => {
return {
@ -141,6 +191,80 @@ describe("GroqHandler", () => {
stream: true,
stream_options: { include_usage: true },
}),
undefined,
)
})
it("should omit temperature when modelTemperature is undefined", async () => {
const modelId: GroqModelId = "llama-3.1-8b-instant"
const handlerWithoutTemp = new GroqHandler({
apiModelId: modelId,
groqApiKey: "test-groq-api-key",
// modelTemperature is not set
})
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}
})
const systemPrompt = "Test system prompt"
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }]
const messageGenerator = handlerWithoutTemp.createMessage(systemPrompt, messages)
await messageGenerator.next()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: modelId,
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
stream: true,
}),
undefined,
)
// Verify temperature is NOT included
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs).not.toHaveProperty("temperature")
})
it("should include temperature when modelTemperature is explicitly set", async () => {
const modelId: GroqModelId = "llama-3.1-8b-instant"
const handlerWithTemp = new GroqHandler({
apiModelId: modelId,
groqApiKey: "test-groq-api-key",
modelTemperature: 0.7,
})
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
async next() {
return { done: true }
},
}),
}
})
const systemPrompt = "Test system prompt"
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message" }]
const messageGenerator = handlerWithTemp.createMessage(systemPrompt, messages)
await messageGenerator.next()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: modelId,
temperature: 0.7,
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
stream: true,
}),
undefined,
)
})
})

View file

@ -1,5 +1,6 @@
// Mock Mistral client - must come before other imports
const mockCreate = vi.fn()
const mockComplete = vi.fn()
vi.mock("@mistralai/mistralai", () => {
return {
Mistral: vi.fn().mockImplementation(() => ({
@ -21,6 +22,17 @@ vi.mock("@mistralai/mistralai", () => {
}
return stream
}),
complete: mockComplete.mockImplementation(async (_options) => {
return {
choices: [
{
message: {
content: "Test response",
},
},
],
}
}),
},
})),
}
@ -29,7 +41,7 @@ vi.mock("@mistralai/mistralai", () => {
import type { Anthropic } from "@anthropic-ai/sdk"
import { MistralHandler } from "../mistral"
import type { ApiHandlerOptions } from "../../../shared/api"
import type { ApiStreamTextChunk } from "../../transform/stream"
import type { ApiStreamTextChunk, ApiStreamReasoningChunk } from "../../transform/stream"
describe("MistralHandler", () => {
let handler: MistralHandler
@ -44,6 +56,7 @@ describe("MistralHandler", () => {
}
handler = new MistralHandler(mockOptions)
mockCreate.mockClear()
mockComplete.mockClear()
})
describe("constructor", () => {
@ -122,5 +135,134 @@ describe("MistralHandler", () => {
mockCreate.mockRejectedValueOnce(new Error("API Error"))
await expect(handler.createMessage(systemPrompt, messages).next()).rejects.toThrow("API Error")
})
it("should handle thinking content as reasoning chunks", async () => {
// Mock stream with thinking content matching new SDK structure
mockCreate.mockImplementationOnce(async (_options) => {
const stream = {
[Symbol.asyncIterator]: async function* () {
yield {
data: {
choices: [
{
delta: {
content: [
{
type: "thinking",
thinking: [{ type: "text", text: "Let me think about this..." }],
},
{ type: "text", text: "Here's the answer" },
],
},
index: 0,
},
],
},
}
},
}
return stream
})
const iterator = handler.createMessage(systemPrompt, messages)
const results: (ApiStreamTextChunk | ApiStreamReasoningChunk)[] = []
for await (const chunk of iterator) {
if ("text" in chunk) {
results.push(chunk as ApiStreamTextChunk | ApiStreamReasoningChunk)
}
}
expect(results).toHaveLength(2)
expect(results[0]).toEqual({ type: "reasoning", text: "Let me think about this..." })
expect(results[1]).toEqual({ type: "text", text: "Here's the answer" })
})
it("should handle mixed content arrays correctly", async () => {
// Mock stream with mixed content matching new SDK structure
mockCreate.mockImplementationOnce(async (_options) => {
const stream = {
[Symbol.asyncIterator]: async function* () {
yield {
data: {
choices: [
{
delta: {
content: [
{ type: "text", text: "First text" },
{
type: "thinking",
thinking: [{ type: "text", text: "Some reasoning" }],
},
{ type: "text", text: "Second text" },
],
},
index: 0,
},
],
},
}
},
}
return stream
})
const iterator = handler.createMessage(systemPrompt, messages)
const results: (ApiStreamTextChunk | ApiStreamReasoningChunk)[] = []
for await (const chunk of iterator) {
if ("text" in chunk) {
results.push(chunk as ApiStreamTextChunk | ApiStreamReasoningChunk)
}
}
expect(results).toHaveLength(3)
expect(results[0]).toEqual({ type: "text", text: "First text" })
expect(results[1]).toEqual({ type: "reasoning", text: "Some reasoning" })
expect(results[2]).toEqual({ type: "text", text: "Second text" })
})
})
describe("completePrompt", () => {
it("should complete prompt successfully", async () => {
const prompt = "Test prompt"
const result = await handler.completePrompt(prompt)
expect(mockComplete).toHaveBeenCalledWith({
model: mockOptions.apiModelId,
messages: [{ role: "user", content: prompt }],
temperature: 0,
})
expect(result).toBe("Test response")
})
it("should filter out thinking content in completePrompt", async () => {
mockComplete.mockImplementationOnce(async (_options) => {
return {
choices: [
{
message: {
content: [
{ type: "thinking", text: "Let me think..." },
{ type: "text", text: "Answer part 1" },
{ type: "text", text: "Answer part 2" },
],
},
},
],
}
})
const prompt = "Test prompt"
const result = await handler.completePrompt(prompt)
expect(result).toBe("Answer part 1Answer part 2")
})
it("should handle errors in completePrompt", async () => {
mockComplete.mockRejectedValueOnce(new Error("API Error"))
await expect(handler.completePrompt("Test prompt")).rejects.toThrow("Mistral completion error: API Error")
})
})
})

View file

@ -315,6 +315,71 @@ describe("OpenAiHandler", () => {
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.max_completion_tokens).toBe(4096)
})
it("should omit temperature when modelTemperature is undefined", async () => {
const optionsWithoutTemperature: ApiHandlerOptions = {
...mockOptions,
// modelTemperature is not set, should not include temperature
}
const handlerWithoutTemperature = new OpenAiHandler(optionsWithoutTemperature)
const stream = handlerWithoutTemperature.createMessage(systemPrompt, messages)
// Consume the stream to trigger the API call
for await (const _chunk of stream) {
}
// Assert the mockCreate was called without temperature
expect(mockCreate).toHaveBeenCalled()
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs).not.toHaveProperty("temperature")
})
it("should include temperature when modelTemperature is explicitly set to 0", async () => {
const optionsWithZeroTemperature: ApiHandlerOptions = {
...mockOptions,
modelTemperature: 0,
}
const handlerWithZeroTemperature = new OpenAiHandler(optionsWithZeroTemperature)
const stream = handlerWithZeroTemperature.createMessage(systemPrompt, messages)
// Consume the stream to trigger the API call
for await (const _chunk of stream) {
}
// Assert the mockCreate was called with temperature: 0
expect(mockCreate).toHaveBeenCalled()
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.temperature).toBe(0)
})
it("should include temperature when modelTemperature is set to a non-zero value", async () => {
const optionsWithCustomTemperature: ApiHandlerOptions = {
...mockOptions,
modelTemperature: 0.7,
}
const handlerWithCustomTemperature = new OpenAiHandler(optionsWithCustomTemperature)
const stream = handlerWithCustomTemperature.createMessage(systemPrompt, messages)
// Consume the stream to trigger the API call
for await (const _chunk of stream) {
}
// Assert the mockCreate was called with temperature: 0.7
expect(mockCreate).toHaveBeenCalled()
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.temperature).toBe(0.7)
})
it("should include DEEP_SEEK_DEFAULT_TEMPERATURE for deepseek-reasoner models when temperature is not set", async () => {
const deepseekOptions: ApiHandlerOptions = {
...mockOptions,
openAiModelId: "deepseek-reasoner",
// modelTemperature is not set
}
const deepseekHandler = new OpenAiHandler(deepseekOptions)
const stream = deepseekHandler.createMessage(systemPrompt, messages)
// Consume the stream to trigger the API call
for await (const _chunk of stream) {
}
// Assert the mockCreate was called with DEEP_SEEK_DEFAULT_TEMPERATURE (0.6)
expect(mockCreate).toHaveBeenCalled()
const callArgs = mockCreate.mock.calls[0][0]
expect(callArgs.temperature).toBe(0.6)
})
})
describe("error handling", () => {
@ -450,7 +515,7 @@ describe("OpenAiHandler", () => {
],
stream: true,
stream_options: { include_usage: true },
temperature: 0,
// temperature should be omitted when not set
},
{ path: "/models/chat/completions" },
)

View file

@ -66,11 +66,11 @@ describe("RequestyHandler", () => {
})
it("can use a base URL instead of the default", () => {
const handler = new RequestyHandler({ ...mockOptions, requestyBaseUrl: "some-base-url" })
const handler = new RequestyHandler({ ...mockOptions, requestyBaseUrl: "https://custom.requesty.ai/v1" })
expect(handler).toBeInstanceOf(RequestyHandler)
expect(OpenAI).toHaveBeenCalledWith({
baseURL: "some-base-url",
baseURL: "https://custom.requesty.ai/v1",
apiKey: mockOptions.requestyApiKey,
defaultHeaders: {
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",

View file

@ -131,21 +131,25 @@ describe("RooHandler", () => {
expect(handler.getModel().id).toBe(mockOptions.apiModelId)
})
it("should throw error if CloudService is not available", () => {
it("should not throw error if CloudService is not available", () => {
mockHasInstanceFn.mockReturnValue(false)
expect(() => {
new RooHandler(mockOptions)
}).toThrow("Authentication required for Roo Code Cloud")
expect(t).toHaveBeenCalledWith("common:errors.roo.authenticationRequired")
}).not.toThrow()
// Constructor should succeed even without CloudService
const handler = new RooHandler(mockOptions)
expect(handler).toBeInstanceOf(RooHandler)
})
it("should throw error if session token is not available", () => {
it("should not throw error if session token is not available", () => {
mockHasInstanceFn.mockReturnValue(true)
mockGetSessionTokenFn.mockReturnValue(null)
expect(() => {
new RooHandler(mockOptions)
}).toThrow("Authentication required for Roo Code Cloud")
expect(t).toHaveBeenCalledWith("common:errors.roo.authenticationRequired")
}).not.toThrow()
// Constructor should succeed even without session token
const handler = new RooHandler(mockOptions)
expect(handler).toBeInstanceOf(RooHandler)
})
it("should initialize with default model if no model specified", () => {
@ -257,6 +261,7 @@ describe("RooHandler", () => {
expect.objectContaining({ role: "user", content: "Second message" }),
]),
}),
undefined,
)
})
})
@ -350,7 +355,7 @@ describe("RooHandler", () => {
})
describe("temperature and model configuration", () => {
it("should use default temperature of 0.7", async () => {
it("should omit temperature when not explicitly set", async () => {
handler = new RooHandler(mockOptions)
const stream = handler.createMessage(systemPrompt, messages)
for await (const _chunk of stream) {
@ -358,9 +363,10 @@ describe("RooHandler", () => {
}
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
temperature: 0.7,
expect.not.objectContaining({
temperature: expect.anything(),
}),
undefined,
)
})
@ -378,6 +384,7 @@ describe("RooHandler", () => {
expect.objectContaining({
temperature: 0.9,
}),
undefined,
)
})
@ -400,7 +407,7 @@ describe("RooHandler", () => {
expect(mockGetSessionTokenFn).toHaveBeenCalled()
})
it("should handle undefined auth service", () => {
it("should handle undefined auth service gracefully", () => {
mockHasInstanceFn.mockReturnValue(true)
// Mock CloudService with undefined authService
const originalGetter = Object.getOwnPropertyDescriptor(CloudService, "instance")?.get
@ -413,7 +420,10 @@ describe("RooHandler", () => {
expect(() => {
new RooHandler(mockOptions)
}).toThrow("Authentication required for Roo Code Cloud")
}).not.toThrow()
// Constructor should succeed even with undefined auth service
const handler = new RooHandler(mockOptions)
expect(handler).toBeInstanceOf(RooHandler)
} finally {
// Always restore original getter, even if test fails
if (originalGetter) {
@ -425,12 +435,15 @@ describe("RooHandler", () => {
}
})
it("should handle empty session token", () => {
it("should handle empty session token gracefully", () => {
mockGetSessionTokenFn.mockReturnValue("")
expect(() => {
new RooHandler(mockOptions)
}).toThrow("Authentication required for Roo Code Cloud")
}).not.toThrow()
// Constructor should succeed even with empty session token
const handler = new RooHandler(mockOptions)
expect(handler).toBeInstanceOf(RooHandler)
})
})
})

View file

@ -144,11 +144,11 @@ describe("SambaNovaHandler", () => {
expect.objectContaining({
model: modelId,
max_tokens: modelInfo.maxTokens,
temperature: 0.7,
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
stream: true,
stream_options: { include_usage: true },
}),
undefined,
)
})
})

View file

@ -220,11 +220,11 @@ describe("ZAiHandler", () => {
expect.objectContaining({
model: modelId,
max_tokens: modelInfo.maxTokens,
temperature: ZAI_DEFAULT_TEMPERATURE,
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
stream: true,
stream_options: { include_usage: true },
}),
undefined,
)
})
})

View file

@ -66,24 +66,27 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
requestOptions?: OpenAI.RequestOptions,
) {
const {
id: model,
info: { maxTokens: max_tokens },
} = this.getModel()
const temperature = this.options.modelTemperature ?? this.defaultTemperature
const params: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model,
max_tokens,
temperature,
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
}
return this.client.chat.completions.create(params)
// Only include temperature if explicitly set
if (this.options.modelTemperature !== undefined) {
params.temperature = this.options.modelTemperature
}
return this.client.chat.completions.create(params, requestOptions)
}
override async *createMessage(

View file

@ -103,7 +103,7 @@ describe("getModels with new GetModelsOptions", () => {
const result = await getModels({ provider: "requesty", apiKey: DUMMY_REQUESTY_KEY })
expect(mockGetRequestyModels).toHaveBeenCalledWith(DUMMY_REQUESTY_KEY)
expect(mockGetRequestyModels).toHaveBeenCalledWith(undefined, DUMMY_REQUESTY_KEY)
expect(result).toEqual(mockModels)
})

View file

@ -24,6 +24,7 @@ describe("OpenRouter API", () => {
const models = await getOpenRouterModels()
const openRouterSupportedCaching = Object.entries(models)
.filter(([id, _]) => id.startsWith("anthropic/claude") || id.startsWith("google/gemini")) // only these support cache_control breakpoints (https://openrouter.ai/docs/features/prompt-caching)
.filter(([_, model]) => model.supportsPromptCache)
.map(([id, _]) => id)
@ -229,7 +230,7 @@ describe("OpenRouter API", () => {
const endpoints = await getOpenRouterModelEndpoints("google/gemini-2.5-pro-preview")
expect(endpoints).toEqual({
Google: {
"google-vertex": {
maxTokens: 65535,
contextWindow: 1048576,
supportsImages: true,
@ -243,7 +244,7 @@ describe("OpenRouter API", () => {
supportsReasoningEffort: undefined,
supportedParameters: undefined,
},
"Google AI Studio": {
"google-ai-studio": {
maxTokens: 65536,
contextWindow: 1048576,
supportsImages: true,

View file

@ -59,7 +59,7 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
break
case "requesty":
// Requesty models endpoint requires an API key for per-user custom policies
models = await getRequestyModels(options.apiKey)
models = await getRequestyModels(options.baseUrl, options.apiKey)
break
case "glama":
models = await getGlamaModels()

View file

@ -58,6 +58,7 @@ export type OpenRouterModel = z.infer<typeof openRouterModelSchema>
export const openRouterModelEndpointSchema = modelRouterBaseModelSchema.extend({
provider_name: z.string(),
tag: z.string().optional(),
})
export type OpenRouterModelEndpoint = z.infer<typeof openRouterModelEndpointSchema>
@ -149,7 +150,7 @@ export async function getOpenRouterModelEndpoints(
const { id, architecture, endpoints } = data
for (const endpoint of endpoints) {
models[endpoint.provider_name] = parseOpenRouterModel({
models[endpoint.tag ?? endpoint.provider_name] = parseOpenRouterModel({
id,
model: endpoint,
modality: architecture?.modality,
@ -188,7 +189,7 @@ export const parseOpenRouterModel = ({
const cacheReadsPrice = model.pricing?.input_cache_read ? parseApiPrice(model.pricing?.input_cache_read) : undefined
const supportsPromptCache = typeof cacheWritesPrice !== "undefined" && typeof cacheReadsPrice !== "undefined"
const supportsPromptCache = typeof cacheReadsPrice !== "undefined" // some models support caching but don't charge a cacheWritesPrice, e.g. GPT-5
const modelInfo: ModelInfo = {
maxTokens: maxTokens || Math.ceil(model.context_length * 0.2),

View file

@ -3,8 +3,9 @@ import axios from "axios"
import type { ModelInfo } from "@roo-code/types"
import { parseApiPrice } from "../../../shared/cost"
import { toRequestyServiceUrl } from "../../../shared/utils/requesty"
export async function getRequestyModels(apiKey?: string): Promise<Record<string, ModelInfo>> {
export async function getRequestyModels(baseUrl?: string, apiKey?: string): Promise<Record<string, ModelInfo>> {
const models: Record<string, ModelInfo> = {}
try {
@ -14,8 +15,10 @@ export async function getRequestyModels(apiKey?: string): Promise<Record<string,
headers["Authorization"] = `Bearer ${apiKey}`
}
const url = "https://router.requesty.ai/v1/models"
const response = await axios.get(url, { headers })
const resolvedBaseUrl = toRequestyServiceUrl(baseUrl)
const modelsUrl = new URL("models", resolvedBaseUrl)
const response = await axios.get(modelsUrl.toString(), { headers })
const rawModels = response.data.data
for (const rawModel of rawModels) {

View file

@ -1,9 +1,22 @@
import { type GroqModelId, groqDefaultModelId, groqModels } from "@roo-code/types"
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import type { ApiHandlerOptions } from "../../shared/api"
import type { ApiHandlerCreateMessageMetadata } from "../index"
import { ApiStream } from "../transform/stream"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { calculateApiCostOpenAI } from "../../shared/cost"
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
// Enhanced usage interface to support Groq's cached token fields
interface GroqUsage extends OpenAI.CompletionUsage {
prompt_tokens_details?: {
cached_tokens?: number
}
}
export class GroqHandler extends BaseOpenAiCompatibleProvider<GroqModelId> {
constructor(options: ApiHandlerOptions) {
super({
@ -16,4 +29,61 @@ export class GroqHandler extends BaseOpenAiCompatibleProvider<GroqModelId> {
defaultTemperature: 0.5,
})
}
override async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
const stream = await this.createStream(systemPrompt, messages, metadata)
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
yield {
type: "text",
text: delta.content,
}
}
if (chunk.usage) {
yield* this.yieldUsage(chunk.usage as GroqUsage)
}
}
}
private async *yieldUsage(usage: GroqUsage | undefined): ApiStream {
const { info } = this.getModel()
const inputTokens = usage?.prompt_tokens || 0
const outputTokens = usage?.completion_tokens || 0
const cacheReadTokens = usage?.prompt_tokens_details?.cached_tokens || 0
// Groq does not track cache writes
const cacheWriteTokens = 0
// Calculate cost using OpenAI-compatible cost calculation
const totalCost = calculateApiCostOpenAI(info, inputTokens, outputTokens, cacheWriteTokens, cacheReadTokens)
// Calculate non-cached input tokens for proper reporting
const nonCachedInputTokens = Math.max(0, inputTokens - cacheReadTokens - cacheWriteTokens)
console.log("usage", {
inputTokens: nonCachedInputTokens,
outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
})
yield {
type: "usage",
inputTokens: nonCachedInputTokens,
outputTokens,
cacheWriteTokens,
cacheReadTokens,
totalCost,
}
}
}

View file

@ -11,6 +11,14 @@ import { ApiStream } from "../transform/stream"
import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
// Type helper to handle thinking chunks from Mistral API
// The SDK includes ThinkChunk but TypeScript has trouble with the discriminated union
type ContentChunkWithThinking = {
type: string
text?: string
thinking?: Array<{ type: string; text?: string }>
}
export class MistralHandler extends BaseProvider implements SingleCompletionHandler {
protected options: ApiHandlerOptions
private client: Mistral
@ -48,26 +56,38 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
temperature,
})
for await (const chunk of response) {
const delta = chunk.data.choices[0]?.delta
for await (const event of response) {
const delta = event.data.choices[0]?.delta
if (delta?.content) {
let content: string = ""
if (typeof delta.content === "string") {
content = delta.content
// Handle string content as text
yield { type: "text", text: delta.content }
} else if (Array.isArray(delta.content)) {
content = delta.content.map((c) => (c.type === "text" ? c.text : "")).join("")
// Handle array of content chunks
// The SDK v1.9.18 supports ThinkChunk with type "thinking"
for (const chunk of delta.content as ContentChunkWithThinking[]) {
if (chunk.type === "thinking" && chunk.thinking) {
// Handle thinking content as reasoning chunks
// ThinkChunk has a 'thinking' property that contains an array of text/reference chunks
for (const thinkingPart of chunk.thinking) {
if (thinkingPart.type === "text" && thinkingPart.text) {
yield { type: "reasoning", text: thinkingPart.text }
}
}
} else if (chunk.type === "text" && chunk.text) {
// Handle text content normally
yield { type: "text", text: chunk.text }
}
}
}
yield { type: "text", text: content }
}
if (chunk.data.usage) {
if (event.data.usage) {
yield {
type: "usage",
inputTokens: chunk.data.usage.promptTokens || 0,
outputTokens: chunk.data.usage.completionTokens || 0,
inputTokens: event.data.usage.promptTokens || 0,
outputTokens: event.data.usage.completionTokens || 0,
}
}
}
@ -97,7 +117,11 @@ export class MistralHandler extends BaseProvider implements SingleCompletionHand
const content = response.choices?.[0]?.message.content
if (Array.isArray(content)) {
return content.map((c) => (c.type === "text" ? c.text : "")).join("")
// Only return text content, filter out thinking content for non-streaming
return (content as ContentChunkWithThinking[])
.filter((c) => c.type === "text" && c.text)
.map((c) => c.text || "")
.join("")
}
return content || ""

View file

@ -157,13 +157,20 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model: modelId,
temperature: this.options.modelTemperature ?? (deepseekReasoner ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0),
messages: convertedMessages,
stream: true as const,
...(isGrokXAI ? {} : { stream_options: { include_usage: true } }),
...(reasoning && reasoning),
}
// Only include temperature if explicitly set
if (this.options.modelTemperature !== undefined) {
requestOptions.temperature = this.options.modelTemperature
} else if (deepseekReasoner) {
// DeepSeek Reasoner has a specific default temperature
requestOptions.temperature = DEEP_SEEK_DEFAULT_TEMPERATURE
}
// Add max_tokens if needed
this.addMaxTokensIfNeeded(requestOptions, modelInfo)

View file

@ -15,6 +15,7 @@ import { DEFAULT_HEADERS } from "./constants"
import { getModels } from "./fetchers/modelCache"
import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import { toRequestyServiceUrl } from "../../shared/utils/requesty"
// Requesty usage includes an extra field for Anthropic use cases.
// Safely cast the prompt token details section to the appropriate structure.
@ -40,21 +41,23 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
protected options: ApiHandlerOptions
protected models: ModelRecord = {}
private client: OpenAI
private baseURL: string
constructor(options: ApiHandlerOptions) {
super()
this.options = options
this.baseURL = toRequestyServiceUrl(options.requestyBaseUrl)
this.client = new OpenAI({
baseURL: options.requestyBaseUrl || "https://router.requesty.ai/v1",
baseURL: this.baseURL,
apiKey: this.options.requestyApiKey ?? "not-provided",
defaultHeaders: DEFAULT_HEADERS,
})
}
public async fetchModel() {
this.models = await getModels({ provider: "requesty" })
this.models = await getModels({ provider: "requesty", baseUrl: this.baseURL })
return this.getModel()
}

View file

@ -4,29 +4,27 @@ import { CloudService } from "@roo-code/cloud"
import type { ApiHandlerOptions } from "../../shared/api"
import { ApiStream } from "../transform/stream"
import { t } from "../../i18n"
import type { ApiHandlerCreateMessageMetadata } from "../index"
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
export class RooHandler extends BaseOpenAiCompatibleProvider<RooModelId> {
constructor(options: ApiHandlerOptions) {
// Check if CloudService is available and get the session token.
if (!CloudService.hasInstance()) {
throw new Error(t("common:errors.roo.authenticationRequired"))
}
const sessionToken = CloudService.instance.authService?.getSessionToken()
if (!sessionToken) {
throw new Error(t("common:errors.roo.authenticationRequired"))
// Get the session token if available, but don't throw if not.
// The server will handle authentication errors and return appropriate status codes.
let sessionToken = ""
if (CloudService.hasInstance()) {
sessionToken = CloudService.instance.authService?.getSessionToken() || ""
}
// Always construct the handler, even without a valid token.
// The provider-proxy server will return 401 if authentication fails.
super({
...options,
providerName: "Roo Code Cloud",
baseURL: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy/v1",
apiKey: sessionToken,
apiKey: sessionToken || "unauthenticated", // Use a placeholder if no token
defaultProviderModelId: rooDefaultModelId,
providerModels: rooModels,
defaultTemperature: 0.7,
@ -38,7 +36,12 @@ export class RooHandler extends BaseOpenAiCompatibleProvider<RooModelId> {
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
const stream = await this.createStream(systemPrompt, messages, metadata)
const stream = await this.createStream(
systemPrompt,
messages,
metadata,
metadata?.taskId ? { headers: { "X-Roo-Task-ID": metadata.taskId } } : undefined,
)
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta

View file

@ -0,0 +1,329 @@
import { describe, it, expect, vi } from "vitest"
import { APIError } from "openai"
import { checkContextWindowExceededError } from "../context-error-handling"
describe("checkContextWindowExceededError", () => {
describe("OpenAI errors", () => {
it("should detect OpenAI context window error with APIError instance", () => {
const error = Object.create(APIError.prototype)
Object.assign(error, {
status: 400,
code: "400",
message: "This model's maximum context length is 4096 tokens",
error: {
message: "This model's maximum context length is 4096 tokens",
type: "invalid_request_error",
param: null,
code: "context_length_exceeded",
},
})
expect(checkContextWindowExceededError(error)).toBe(true)
})
it("should detect OpenAI LengthFinishReasonError", () => {
const error = {
name: "LengthFinishReasonError",
message: "The response was cut off due to length",
}
expect(checkContextWindowExceededError(error)).toBe(true)
})
it("should not detect non-context OpenAI errors", () => {
const error = Object.create(APIError.prototype)
Object.assign(error, {
status: 400,
code: "400",
message: "Invalid API key",
error: {
message: "Invalid API key",
type: "invalid_request_error",
param: null,
code: "invalid_api_key",
},
})
expect(checkContextWindowExceededError(error)).toBe(false)
})
})
describe("OpenRouter errors", () => {
it("should detect OpenRouter context window error with status 400", () => {
const error = {
status: 400,
message: "Request exceeds maximum context length of 8192 tokens",
}
expect(checkContextWindowExceededError(error)).toBe(true)
})
it("should detect OpenRouter error with nested error structure", () => {
const error = {
error: {
status: 400,
message: "Input tokens exceed model limit",
},
}
expect(checkContextWindowExceededError(error)).toBe(true)
})
it("should detect OpenRouter error with response status", () => {
const error = {
response: {
status: 400,
},
message: "Too many tokens in the request",
}
expect(checkContextWindowExceededError(error)).toBe(true)
})
it("should detect various context error patterns", () => {
const patterns = [
"context length exceeded",
"maximum context window",
"input tokens exceed limit",
"too many tokens",
]
patterns.forEach((pattern) => {
const error = {
status: 400,
message: pattern,
}
expect(checkContextWindowExceededError(error)).toBe(true)
})
})
it("should not detect non-context 400 errors", () => {
const error = {
status: 400,
message: "Invalid request format",
}
expect(checkContextWindowExceededError(error)).toBe(false)
})
it("should not detect errors with different status codes", () => {
const error = {
status: 500,
message: "context length exceeded",
}
expect(checkContextWindowExceededError(error)).toBe(false)
})
})
describe("Anthropic errors", () => {
it("should detect Anthropic context window error", () => {
const error = {
error: {
error: {
type: "invalid_request_error",
message: "prompt is too long: 150000 tokens > 100000 maximum",
},
},
}
expect(checkContextWindowExceededError(error)).toBe(true)
})
it("should detect Anthropic error with context_length_exceeded code", () => {
const error = {
error: {
error: {
type: "invalid_request_error",
code: "context_length_exceeded",
message: "The request exceeds the maximum context window",
},
},
}
expect(checkContextWindowExceededError(error)).toBe(true)
})
it("should detect various Anthropic context error patterns", () => {
const patterns = [
"prompt is too long",
"maximum 200000 tokens",
"context is too long",
"exceeds the context window",
"token limit exceeded",
]
patterns.forEach((pattern) => {
const error = {
error: {
error: {
type: "invalid_request_error",
message: pattern,
},
},
}
expect(checkContextWindowExceededError(error)).toBe(true)
})
})
it("should not detect non-context Anthropic errors", () => {
const error = {
error: {
error: {
type: "invalid_request_error",
message: "Invalid model specified",
},
},
}
expect(checkContextWindowExceededError(error)).toBe(false)
})
it("should not detect errors with different error types", () => {
const error = {
error: {
error: {
type: "authentication_error",
message: "prompt is too long",
},
},
}
expect(checkContextWindowExceededError(error)).toBe(false)
})
})
describe("Cerebras errors", () => {
it("should detect Cerebras context window error", () => {
const error = {
status: 400,
message: "Please reduce the length of the messages or completion",
}
expect(checkContextWindowExceededError(error)).toBe(true)
})
it("should detect Cerebras error with nested structure", () => {
const error = {
error: {
status: 400,
message: "Please reduce the length of the messages or completion",
},
}
expect(checkContextWindowExceededError(error)).toBe(true)
})
it("should not detect non-context Cerebras errors", () => {
const error = {
status: 400,
message: "Invalid request parameters",
}
expect(checkContextWindowExceededError(error)).toBe(false)
})
})
describe("Edge cases", () => {
it("should handle null input", () => {
expect(checkContextWindowExceededError(null)).toBe(false)
})
it("should handle undefined input", () => {
expect(checkContextWindowExceededError(undefined)).toBe(false)
})
it("should handle empty object", () => {
expect(checkContextWindowExceededError({})).toBe(false)
})
it("should handle string input", () => {
expect(checkContextWindowExceededError("error")).toBe(false)
})
it("should handle number input", () => {
expect(checkContextWindowExceededError(123)).toBe(false)
})
it("should handle array input", () => {
expect(checkContextWindowExceededError([])).toBe(false)
})
it("should handle errors with circular references", () => {
const error: any = { status: 400, message: "context length exceeded" }
error.self = error // Create circular reference
expect(checkContextWindowExceededError(error)).toBe(true)
})
it("should handle errors with deeply nested undefined values", () => {
const error = {
error: {
error: {
type: undefined,
message: undefined,
},
},
}
expect(checkContextWindowExceededError(error)).toBe(false)
})
it("should handle errors that throw during property access", () => {
const error = {
get status() {
throw new Error("Property access error")
},
message: "context length exceeded",
}
expect(checkContextWindowExceededError(error)).toBe(false)
})
it("should handle mixed provider error structures", () => {
// Error that could match multiple providers
const error = {
status: 400,
code: "400",
message: "context length exceeded",
error: {
error: {
type: "invalid_request_error",
message: "prompt is too long",
},
},
}
expect(checkContextWindowExceededError(error)).toBe(true)
})
})
describe("Multiple provider detection", () => {
it("should detect error if any provider check returns true", () => {
// This error should be detected by OpenRouter check
const error1 = {
status: 400,
message: "context window exceeded",
}
expect(checkContextWindowExceededError(error1)).toBe(true)
// This error should be detected by Anthropic check
const error2 = {
error: {
error: {
type: "invalid_request_error",
message: "prompt is too long",
},
},
}
expect(checkContextWindowExceededError(error2)).toBe(true)
// This error should be detected by Cerebras check
const error3 = {
status: 400,
message: "Please reduce the length of the messages or completion",
}
expect(checkContextWindowExceededError(error3)).toBe(true)
})
})
})

View file

@ -0,0 +1,114 @@
import { APIError } from "openai"
export function checkContextWindowExceededError(error: unknown): boolean {
return (
checkIsOpenAIContextWindowError(error) ||
checkIsOpenRouterContextWindowError(error) ||
checkIsAnthropicContextWindowError(error) ||
checkIsCerebrasContextWindowError(error)
)
}
function checkIsOpenRouterContextWindowError(error: unknown): boolean {
try {
if (!error || typeof error !== "object") {
return false
}
// Use Record<string, any> for proper type narrowing
const err = error as Record<string, any>
const status = err.status ?? err.code ?? err.error?.status ?? err.response?.status
const message: string = String(err.message || err.error?.message || "")
// Known OpenAI/OpenRouter-style signal (code 400 and message includes "context length")
const CONTEXT_ERROR_PATTERNS = [
/\bcontext\s*(?:length|window)\b/i,
/\bmaximum\s*context\b/i,
/\b(?:input\s*)?tokens?\s*exceed/i,
/\btoo\s*many\s*tokens?\b/i,
] as const
return String(status) === "400" && CONTEXT_ERROR_PATTERNS.some((pattern) => pattern.test(message))
} catch {
return false
}
}
// Docs: https://platform.openai.com/docs/guides/error-codes/api-errors
function checkIsOpenAIContextWindowError(error: unknown): boolean {
try {
// Check for LengthFinishReasonError
if (error && typeof error === "object" && "name" in error && error.name === "LengthFinishReasonError") {
return true
}
const KNOWN_CONTEXT_ERROR_SUBSTRINGS = ["token", "context length"] as const
return (
Boolean(error) &&
error instanceof APIError &&
error.code?.toString() === "400" &&
KNOWN_CONTEXT_ERROR_SUBSTRINGS.some((substring) => error.message.includes(substring))
)
} catch {
return false
}
}
function checkIsAnthropicContextWindowError(response: unknown): boolean {
try {
// Type guard to safely access properties
if (!response || typeof response !== "object") {
return false
}
// Use type assertions with proper checks
const res = response as Record<string, any>
// Check for Anthropic-specific error structure with more specific validation
if (res.error?.error?.type === "invalid_request_error") {
const message: string = String(res.error?.error?.message || "")
// More specific patterns for context window errors
const contextWindowPatterns = [
/prompt is too long/i,
/maximum.*tokens/i,
/context.*too.*long/i,
/exceeds.*context/i,
/token.*limit/i,
/context_length_exceeded/i,
/max_tokens_to_sample/i,
]
// Additional check for Anthropic-specific error codes
const errorCode = res.error?.error?.code
if (errorCode === "context_length_exceeded" || errorCode === "invalid_request_error") {
return contextWindowPatterns.some((pattern) => pattern.test(message))
}
return contextWindowPatterns.some((pattern) => pattern.test(message))
}
return false
} catch {
return false
}
}
function checkIsCerebrasContextWindowError(response: unknown): boolean {
try {
// Type guard to safely access properties
if (!response || typeof response !== "object") {
return false
}
// Use type assertions with proper checks
const res = response as Record<string, any>
const status = res.status ?? res.code ?? res.error?.status ?? res.response?.status
const message: string = String(res.message || res.error?.message || "")
return String(status) === "400" && message.includes("Please reduce the length of the messages or completion")
} catch {
return false
}
}

View file

@ -22,13 +22,6 @@ Tool uses are formatted using XML-style tags. The tool name itself becomes the X
...
</actual_tool_name>
For example, to use the new_task tool:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
</new_task>
Always use the actual tool name as the XML tag name for proper parsing and execution.
# Tools
@ -347,7 +340,7 @@ Usage:
Example:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
<message>Implement a new feature for the application</message>
</new_task>

View file

@ -22,13 +22,6 @@ Tool uses are formatted using XML-style tags. The tool name itself becomes the X
...
</actual_tool_name>
For example, to use the new_task tool:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
</new_task>
Always use the actual tool name as the XML tag name for proper parsing and execution.
# Tools
@ -244,7 +237,7 @@ Usage:
Example:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
<message>Implement a new feature for the application</message>
</new_task>

View file

@ -22,13 +22,6 @@ Tool uses are formatted using XML-style tags. The tool name itself becomes the X
...
</actual_tool_name>
For example, to use the new_task tool:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
</new_task>
Always use the actual tool name as the XML tag name for proper parsing and execution.
# Tools
@ -346,7 +339,7 @@ Usage:
Example:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
<message>Implement a new feature for the application</message>
</new_task>

View file

@ -22,13 +22,6 @@ Tool uses are formatted using XML-style tags. The tool name itself becomes the X
...
</actual_tool_name>
For example, to use the new_task tool:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
</new_task>
Always use the actual tool name as the XML tag name for proper parsing and execution.
# Tools
@ -396,7 +389,7 @@ Usage:
Example:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
<message>Implement a new feature for the application</message>
</new_task>

View file

@ -22,13 +22,6 @@ Tool uses are formatted using XML-style tags. The tool name itself becomes the X
...
</actual_tool_name>
For example, to use the new_task tool:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
</new_task>
Always use the actual tool name as the XML tag name for proper parsing and execution.
# Tools
@ -352,7 +345,7 @@ Usage:
Example:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
<message>Implement a new feature for the application</message>
</new_task>

View file

@ -22,13 +22,6 @@ Tool uses are formatted using XML-style tags. The tool name itself becomes the X
...
</actual_tool_name>
For example, to use the new_task tool:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
</new_task>
Always use the actual tool name as the XML tag name for proper parsing and execution.
# Tools
@ -347,7 +340,7 @@ Usage:
Example:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
<message>Implement a new feature for the application</message>
</new_task>

View file

@ -22,13 +22,6 @@ Tool uses are formatted using XML-style tags. The tool name itself becomes the X
...
</actual_tool_name>
For example, to use the new_task tool:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
</new_task>
Always use the actual tool name as the XML tag name for proper parsing and execution.
# Tools
@ -400,7 +393,7 @@ Usage:
Example:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
<message>Implement a new feature for the application</message>
</new_task>

View file

@ -22,13 +22,6 @@ Tool uses are formatted using XML-style tags. The tool name itself becomes the X
...
</actual_tool_name>
For example, to use the new_task tool:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
</new_task>
Always use the actual tool name as the XML tag name for proper parsing and execution.
# Tools
@ -347,7 +340,7 @@ Usage:
Example:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
<message>Implement a new feature for the application</message>
</new_task>

View file

@ -22,13 +22,6 @@ Tool uses are formatted using XML-style tags. The tool name itself becomes the X
...
</actual_tool_name>
For example, to use the new_task tool:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
</new_task>
Always use the actual tool name as the XML tag name for proper parsing and execution.
# Tools
@ -435,7 +428,7 @@ Usage:
Example:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
<message>Implement a new feature for the application</message>
</new_task>

View file

@ -22,13 +22,6 @@ Tool uses are formatted using XML-style tags. The tool name itself becomes the X
...
</actual_tool_name>
For example, to use the new_task tool:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
</new_task>
Always use the actual tool name as the XML tag name for proper parsing and execution.
# Tools
@ -347,7 +340,7 @@ Usage:
Example:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
<message>Implement a new feature for the application</message>
</new_task>

View file

@ -22,13 +22,6 @@ Tool uses are formatted using XML-style tags. The tool name itself becomes the X
...
</actual_tool_name>
For example, to use the new_task tool:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
</new_task>
Always use the actual tool name as the XML tag name for proper parsing and execution.
# Tools
@ -400,7 +393,7 @@ Usage:
Example:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
<message>Implement a new feature for the application</message>
</new_task>

View file

@ -22,13 +22,6 @@ Tool uses are formatted using XML-style tags. The tool name itself becomes the X
...
</actual_tool_name>
For example, to use the new_task tool:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
</new_task>
Always use the actual tool name as the XML tag name for proper parsing and execution.
# Tools
@ -396,7 +389,7 @@ Usage:
Example:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
<message>Implement a new feature for the application</message>
</new_task>

View file

@ -22,13 +22,6 @@ Tool uses are formatted using XML-style tags. The tool name itself becomes the X
...
</actual_tool_name>
For example, to use the new_task tool:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
</new_task>
Always use the actual tool name as the XML tag name for proper parsing and execution.
# Tools
@ -347,7 +340,7 @@ Usage:
Example:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
<message>Implement a new feature for the application</message>
</new_task>

View file

@ -580,6 +580,7 @@ describe("SYSTEM_PROMPT", () => {
maxConcurrentFileReads: 5,
todoListEnabled: false,
useAgentRules: true,
newTaskRequireTodos: false,
}
const prompt = await SYSTEM_PROMPT(
@ -612,6 +613,7 @@ describe("SYSTEM_PROMPT", () => {
maxConcurrentFileReads: 5,
todoListEnabled: true,
useAgentRules: true,
newTaskRequireTodos: false,
}
const prompt = await SYSTEM_PROMPT(
@ -643,6 +645,7 @@ describe("SYSTEM_PROMPT", () => {
maxConcurrentFileReads: 5,
todoListEnabled: true,
useAgentRules: true,
newTaskRequireTodos: false,
}
const prompt = await SYSTEM_PROMPT(

View file

@ -535,7 +535,14 @@ describe("addCustomInstructions", () => {
"global instructions",
"/fake/path",
"test-mode",
{ settings: { maxConcurrentFileReads: 5, todoListEnabled: true, useAgentRules: true } },
{
settings: {
maxConcurrentFileReads: 5,
todoListEnabled: true,
useAgentRules: true,
newTaskRequireTodos: false,
},
},
)
expect(result).toContain("# Agent Rules Standard (AGENTS.md):")
@ -560,7 +567,14 @@ describe("addCustomInstructions", () => {
"global instructions",
"/fake/path",
"test-mode",
{ settings: { maxConcurrentFileReads: 5, todoListEnabled: true, useAgentRules: false } },
{
settings: {
maxConcurrentFileReads: 5,
todoListEnabled: true,
useAgentRules: false,
newTaskRequireTodos: false,
},
},
)
expect(result).not.toContain("# Agent Rules Standard (AGENTS.md):")
@ -614,7 +628,14 @@ describe("addCustomInstructions", () => {
"global instructions",
"/fake/path",
"test-mode",
{ settings: { maxConcurrentFileReads: 5, todoListEnabled: true, useAgentRules: true } },
{
settings: {
maxConcurrentFileReads: 5,
todoListEnabled: true,
useAgentRules: true,
newTaskRequireTodos: false,
},
},
)
expect(result).toContain("Global Instructions:\nglobal instructions")
@ -653,7 +674,14 @@ describe("addCustomInstructions", () => {
"global instructions",
"/fake/path",
"test-mode",
{ settings: { maxConcurrentFileReads: 5, todoListEnabled: true, useAgentRules: true } },
{
settings: {
maxConcurrentFileReads: 5,
todoListEnabled: true,
useAgentRules: true,
newTaskRequireTodos: false,
},
},
)
// Should contain both AGENTS.md and .roorules content
@ -714,7 +742,14 @@ describe("addCustomInstructions", () => {
"global instructions",
"/fake/path",
"test-mode",
{ settings: { maxConcurrentFileReads: 5, todoListEnabled: true, useAgentRules: true } },
{
settings: {
maxConcurrentFileReads: 5,
todoListEnabled: true,
useAgentRules: true,
newTaskRequireTodos: false,
},
},
)
expect(result).toContain("# Agent Rules Standard (AGENTS.md):")
@ -759,7 +794,14 @@ describe("addCustomInstructions", () => {
"global instructions",
"/fake/path",
"test-mode",
{ settings: { maxConcurrentFileReads: 5, todoListEnabled: true, useAgentRules: true } },
{
settings: {
maxConcurrentFileReads: 5,
todoListEnabled: true,
useAgentRules: true,
newTaskRequireTodos: false,
},
},
)
expect(result).toContain("# Agent Rules Standard (AGENTS.md):")
@ -806,7 +848,14 @@ describe("addCustomInstructions", () => {
"global instructions",
"/fake/path",
"test-mode",
{ settings: { maxConcurrentFileReads: 5, todoListEnabled: true, useAgentRules: true } },
{
settings: {
maxConcurrentFileReads: 5,
todoListEnabled: true,
useAgentRules: true,
newTaskRequireTodos: false,
},
},
)
expect(result).toContain("# Agent Rules Standard (AGENT.md):")
@ -845,7 +894,14 @@ describe("addCustomInstructions", () => {
"global instructions",
"/fake/path",
"test-mode",
{ settings: { maxConcurrentFileReads: 5, todoListEnabled: true, useAgentRules: true } },
{
settings: {
maxConcurrentFileReads: 5,
todoListEnabled: true,
useAgentRules: true,
newTaskRequireTodos: false,
},
},
)
// Should contain AGENTS.md content (preferred) and not AGENT.md

View file

@ -15,12 +15,5 @@ Tool uses are formatted using XML-style tags. The tool name itself becomes the X
...
</actual_tool_name>
For example, to use the new_task tool:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
</new_task>
Always use the actual tool name as the XML tag name for proper parsing and execution.`
}

View file

@ -0,0 +1,128 @@
import { describe, it, expect } from "vitest"
import { getNewTaskDescription } from "../new-task"
import { ToolArgs } from "../types"
describe("getNewTaskDescription", () => {
it("should NOT show todos parameter at all when setting is disabled", () => {
const args: ToolArgs = {
cwd: "/test",
supportsComputerUse: false,
settings: {
newTaskRequireTodos: false,
},
}
const description = getNewTaskDescription(args)
// Check that todos parameter is NOT shown at all
expect(description).not.toContain("todos:")
expect(description).not.toContain("todos parameter")
expect(description).not.toContain("The initial todo list in markdown checklist format")
// Should have a simple example without todos
expect(description).toContain("Implement a new feature for the application")
// Should NOT have any todos tags in examples
expect(description).not.toContain("<todos>")
expect(description).not.toContain("</todos>")
// Should still have mode and message as required
expect(description).toContain("mode: (required)")
expect(description).toContain("message: (required)")
})
it("should show todos as required when setting is enabled", () => {
const args: ToolArgs = {
cwd: "/test",
supportsComputerUse: false,
settings: {
newTaskRequireTodos: true,
},
}
const description = getNewTaskDescription(args)
// Check that todos is marked as required
expect(description).toContain("todos: (required)")
expect(description).toContain("and initial todo list")
expect(description).toContain("The initial todo list in markdown checklist format")
// Should not contain any mention of optional for todos
expect(description).not.toContain("todos: (optional)")
expect(description).not.toContain("optional initial todo list")
// Should include todos in the example
expect(description).toContain("<todos>")
expect(description).toContain("</todos>")
expect(description).toContain("Set up auth middleware")
})
it("should NOT show todos parameter when settings is undefined", () => {
const args: ToolArgs = {
cwd: "/test",
supportsComputerUse: false,
settings: undefined,
}
const description = getNewTaskDescription(args)
// Check that todos parameter is NOT shown by default
expect(description).not.toContain("todos:")
expect(description).not.toContain("The initial todo list in markdown checklist format")
expect(description).not.toContain("<todos>")
expect(description).not.toContain("</todos>")
})
it("should NOT show todos parameter when newTaskRequireTodos is undefined", () => {
const args: ToolArgs = {
cwd: "/test",
supportsComputerUse: false,
settings: {},
}
const description = getNewTaskDescription(args)
// Check that todos parameter is NOT shown by default
expect(description).not.toContain("todos:")
expect(description).not.toContain("The initial todo list in markdown checklist format")
expect(description).not.toContain("<todos>")
expect(description).not.toContain("</todos>")
})
it("should include todos in examples only when setting is enabled", () => {
const argsWithSettingOff: ToolArgs = {
cwd: "/test",
supportsComputerUse: false,
settings: {
newTaskRequireTodos: false,
},
}
const argsWithSettingOn: ToolArgs = {
cwd: "/test",
supportsComputerUse: false,
settings: {
newTaskRequireTodos: true,
},
}
const descriptionOff = getNewTaskDescription(argsWithSettingOff)
const descriptionOn = getNewTaskDescription(argsWithSettingOn)
// When setting is on, should include todos in main example
expect(descriptionOn).toContain("Implement user authentication")
expect(descriptionOn).toContain("[ ] Set up auth middleware")
expect(descriptionOn).toContain("<todos>")
expect(descriptionOn).toContain("</todos>")
// When setting is off, should NOT include any todos references
expect(descriptionOff).not.toContain("<todos>")
expect(descriptionOff).not.toContain("</todos>")
expect(descriptionOff).not.toContain("[ ] Set up auth middleware")
expect(descriptionOff).not.toContain("[ ] First task to complete")
// When setting is off, main example should be simple
const usagePattern = /<new_task>\s*<mode>.*<\/mode>\s*<message>.*<\/message>\s*<\/new_task>/s
expect(descriptionOff).toMatch(usagePattern)
})
})

View file

@ -1,7 +1,9 @@
import { ToolArgs } from "./types"
export function getNewTaskDescription(_args: ToolArgs): string {
return `## new_task
/**
* Prompt when todos are NOT required (default)
*/
const PROMPT_WITHOUT_TODOS = `## new_task
Description: This will let you create a new task instance in the chosen mode using your provided message.
Parameters:
@ -17,7 +19,49 @@ Usage:
Example:
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
<message>Implement a new feature for the application</message>
</new_task>
`
/**
* Prompt when todos ARE required
*/
const PROMPT_WITH_TODOS = `## new_task
Description: This will let you create a new task instance in the chosen mode using your provided message and initial todo list.
Parameters:
- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
- message: (required) The initial user message or instructions for this new task.
- todos: (required) The initial todo list in markdown checklist format for the new task.
Usage:
<new_task>
<mode>your-mode-slug-here</mode>
<message>Your initial instructions here</message>
<todos>
[ ] First task to complete
[ ] Second task to complete
[ ] Third task to complete
</todos>
</new_task>
Example:
<new_task>
<mode>code</mode>
<message>Implement user authentication</message>
<todos>
[ ] Set up auth middleware
[ ] Create login endpoint
[ ] Add session management
[ ] Write tests
</todos>
</new_task>
`
export function getNewTaskDescription(args: ToolArgs): string {
const todosRequired = args.settings?.newTaskRequireTodos === true
// Simply return the appropriate prompt based on the setting
return todosRequired ? PROMPT_WITH_TODOS : PROMPT_WITHOUT_TODOS
}

View file

@ -5,4 +5,5 @@ export interface SystemPromptSettings {
maxConcurrentFileReads: number
todoListEnabled: boolean
useAgentRules: boolean
newTaskRequireTodos: boolean
}

View file

@ -80,7 +80,7 @@ import { ToolRepetitionDetector } from "../tools/ToolRepetitionDetector"
import { FileContextTracker } from "../context-tracking/FileContextTracker"
import { RooIgnoreController } from "../ignore/RooIgnoreController"
import { RooProtectedController } from "../protect/RooProtectedController"
import { type AssistantMessageContent, presentAssistantMessage, parseAssistantMessage } from "../assistant-message"
import { type AssistantMessageContent, presentAssistantMessage } from "../assistant-message"
import { AssistantMessageParser } from "../assistant-message/AssistantMessageParser"
import { truncateConversationIfNeeded } from "../sliding-window"
import { ClineProvider } from "../webview/ClineProvider"
@ -88,6 +88,7 @@ import { MultiSearchReplaceDiffStrategy } from "../diff/strategies/multi-search-
import { MultiFileSearchReplaceDiffStrategy } from "../diff/strategies/multi-file-search-replace"
import { readApiMessages, saveApiMessages, readTaskMessages, saveTaskMessages, taskMetadata } from "../task-persistence"
import { getEnvironmentDetails } from "../environment/getEnvironmentDetails"
import { checkContextWindowExceededError } from "../context/context-management/context-error-handling"
import {
type CheckpointDiffOptions,
type CheckpointRestoreOptions,
@ -105,6 +106,8 @@ import { AutoApprovalHandler } from "./AutoApprovalHandler"
const MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 // 10 minutes
const DEFAULT_USAGE_COLLECTION_TIMEOUT_MS = 5000 // 5 seconds
const FORCED_CONTEXT_REDUCTION_PERCENT = 75 // Keep 75% of context (remove 25%) on context window errors
const MAX_CONTEXT_WINDOW_RETRIES = 3 // Maximum retries for context window errors
export type TaskOptions = {
provider: ClineProvider
@ -123,6 +126,7 @@ export type TaskOptions = {
parentTask?: Task
taskNumber?: number
onCreated?: (task: Task) => void
initialTodos?: TodoItem[]
}
export class Task extends EventEmitter<TaskEvents> implements TaskLike {
@ -266,8 +270,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
didRejectTool = false
didAlreadyUseTool = false
didCompleteReadingStream = false
assistantMessageParser?: AssistantMessageParser
isAssistantMessageParserEnabled = false
assistantMessageParser: AssistantMessageParser
private lastUsedInstructions?: string
private skipPrevResponseIdOnce: boolean = false
@ -287,6 +290,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
parentTask,
taskNumber = -1,
onCreated,
initialTodos,
}: TaskOptions) {
super()
@ -350,6 +354,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
TelemetryService.instance.captureTaskCreated(this.taskId)
}
// Initialize the assistant message parser
this.assistantMessageParser = new AssistantMessageParser()
// Only set up diff strategy if diff is enabled.
if (this.diffEnabled) {
// Default to old strategy, will be updated if experiment is enabled.
@ -370,6 +377,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.toolRepetitionDetector = new ToolRepetitionDetector(this.consecutiveMistakeLimit)
// Initialize todo list if provided
if (initialTodos && initialTodos.length > 0) {
this.todoList = initialTodos
}
onCreated?.(this)
if (startTask) {
@ -1105,6 +1117,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// messages from previous session).
this.clineMessages = []
this.apiConversationHistory = []
// The todo list is already set in the constructor if initialTodos were provided
// No need to add any messages - the todoList property is already set
await this.providerRef.deref()?.postStateToWebview()
await this.say("text", task, images)
@ -1137,6 +1153,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
role: "user",
content: [{ type: "text", text: `[new_task completed] Result: ${lastMessage}` }],
})
// Set skipPrevResponseIdOnce to ensure the next API call sends the full conversation
// including the subtask result, not just from before the subtask was created
this.skipPrevResponseIdOnce = true
} catch (error) {
this.providerRef
.deref()
@ -1417,7 +1437,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
if (this.bridgeService) {
this.bridgeService
.unsubscribeFromTask(this.taskId)
.catch((error) => console.error("Error unsubscribing from task bridge:", error))
.catch((error: unknown) => console.error("Error unsubscribing from task bridge:", error))
this.bridgeService = null
}
@ -1767,9 +1787,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.didAlreadyUseTool = false
this.presentAssistantMessageLocked = false
this.presentAssistantMessageHasPendingUpdates = false
if (this.assistantMessageParser) {
this.assistantMessageParser.reset()
}
this.assistantMessageParser.reset()
await this.diffViewProvider.reset()
@ -1810,12 +1828,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Parse raw assistant message chunk into content blocks.
const prevLength = this.assistantMessageContent.length
if (this.isAssistantMessageParserEnabled && this.assistantMessageParser) {
this.assistantMessageContent = this.assistantMessageParser.processChunk(chunk.text)
} else {
// Use the old parsing method when experiment is disabled
this.assistantMessageContent = parseAssistantMessage(assistantMessage)
}
this.assistantMessageContent = this.assistantMessageParser.processChunk(chunk.text)
if (this.assistantMessageContent.length > prevLength) {
// New content we need to present, reset to
@ -2074,11 +2087,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// this.assistantMessageContent.forEach((e) => (e.partial = false))
// Now that the stream is complete, finalize any remaining partial content blocks
if (this.isAssistantMessageParserEnabled && this.assistantMessageParser) {
this.assistantMessageParser.finalizeContentBlocks()
this.assistantMessageContent = this.assistantMessageParser.getContentBlocks()
}
// When using old parser, no finalization needed - parsing already happened during streaming
this.assistantMessageParser.finalizeContentBlocks()
this.assistantMessageContent = this.assistantMessageParser.getContentBlocks()
if (partialBlocks.length > 0) {
// If there is content to update then it will complete and
@ -2097,9 +2107,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
await this.providerRef.deref()?.postStateToWebview()
// Reset parser after each complete conversation round
if (this.assistantMessageParser) {
this.assistantMessageParser.reset()
}
this.assistantMessageParser.reset()
// Now add to apiConversationHistory.
// Need to save assistant responses to file before proceeding to
@ -2255,6 +2263,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
maxConcurrentFileReads: maxConcurrentFileReads ?? 5,
todoListEnabled: apiConfiguration?.todoListEnabled ?? true,
useAgentRules: vscode.workspace.getConfiguration("roo-cline").get<boolean>("useAgentRules") ?? true,
newTaskRequireTodos: vscode.workspace
.getConfiguration("roo-cline")
.get<boolean>("newTaskRequireTodos", false),
},
undefined, // todoList
this.api.getModel().id,
@ -2262,6 +2273,71 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
})()
}
private getCurrentProfileId(state: any): string {
return (
state?.listApiConfigMeta?.find((profile: any) => profile.name === state?.currentApiConfigName)?.id ??
"default"
)
}
private async handleContextWindowExceededError(): Promise<void> {
const state = await this.providerRef.deref()?.getState()
const { profileThresholds = {} } = state ?? {}
const { contextTokens } = this.getTokenUsage()
const modelInfo = this.api.getModel().info
const maxTokens = getModelMaxOutputTokens({
modelId: this.api.getModel().id,
model: modelInfo,
settings: this.apiConfiguration,
})
const contextWindow = modelInfo.contextWindow
// Get the current profile ID using the helper method
const currentProfileId = this.getCurrentProfileId(state)
// Log the context window error for debugging
console.warn(
`[Task#${this.taskId}] Context window exceeded for model ${this.api.getModel().id}. ` +
`Current tokens: ${contextTokens}, Context window: ${contextWindow}. ` +
`Forcing truncation to ${FORCED_CONTEXT_REDUCTION_PERCENT}% of current context.`,
)
// Force aggressive truncation by keeping only 75% of the conversation history
const truncateResult = await truncateConversationIfNeeded({
messages: this.apiConversationHistory,
totalTokens: contextTokens || 0,
maxTokens,
contextWindow,
apiHandler: this.api,
autoCondenseContext: true,
autoCondenseContextPercent: FORCED_CONTEXT_REDUCTION_PERCENT,
systemPrompt: await this.getSystemPrompt(),
taskId: this.taskId,
profileThresholds,
currentProfileId,
})
if (truncateResult.messages !== this.apiConversationHistory) {
await this.overwriteApiConversationHistory(truncateResult.messages)
}
if (truncateResult.summary) {
const { summary, cost, prevContextTokens, newContextTokens = 0 } = truncateResult
const contextCondense: ContextCondense = { summary, cost, newContextTokens, prevContextTokens }
await this.say(
"condense_context",
undefined /* text */,
undefined /* images */,
false /* partial */,
undefined /* checkpoint */,
undefined /* progressStatus */,
{ isNonInteractive: true } /* options */,
contextCondense,
)
}
}
public async *attemptApiRequest(retryAttempt: number = 0): ApiStream {
const state = await this.providerRef.deref()?.getState()
@ -2340,9 +2416,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
const contextWindow = modelInfo.contextWindow
const currentProfileId =
state?.listApiConfigMeta.find((profile) => profile.name === state?.currentApiConfigName)?.id ??
"default"
// Get the current profile ID using the helper method
const currentProfileId = this.getCurrentProfileId(state)
const truncateResult = await truncateConversationIfNeeded({
messages: this.apiConversationHistory,
@ -2449,6 +2524,21 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.isWaitingForFirstChunk = false
} catch (error) {
this.isWaitingForFirstChunk = false
const isContextWindowExceededError = checkContextWindowExceededError(error)
// If it's a context window error and we haven't exceeded max retries for this error type
if (isContextWindowExceededError && retryAttempt < MAX_CONTEXT_WINDOW_RETRIES) {
console.warn(
`[Task#${this.taskId}] Context window exceeded for model ${this.api.getModel().id}. ` +
`Retry attempt ${retryAttempt + 1}/${MAX_CONTEXT_WINDOW_RETRIES}. ` +
`Attempting automatic truncation...`,
)
await this.handleContextWindowExceededError()
// Retry the request after handling the context window error
yield* this.attemptApiRequest(retryAttempt + 1)
return
}
// note that this api_req_failed ask is unique in that we only present this option if the api hasn't streamed any content yet (ie it fails on the first chunk due), as it would allow them to hit a retry button. However if the api failed mid-stream, it could be in any arbitrary state where some tools may have executed, so that error is handled differently and requires cancelling the task entirely.
if (autoApprovalEnabled && alwaysApproveResubmit) {
let errorMsg

View file

@ -2,6 +2,25 @@
import type { AskApproval, HandleError } from "../../../shared/tools"
// Mock vscode module
vi.mock("vscode", () => ({
workspace: {
getConfiguration: vi.fn(() => ({
get: vi.fn(),
})),
},
}))
// Mock Package module
vi.mock("../../../shared/package", () => ({
Package: {
name: "roo-cline",
publisher: "RooVeterinaryInc",
version: "1.0.0",
outputChannel: "Roo-Code",
},
}))
// Mock other modules first - these are hoisted to the top
vi.mock("../../../shared/modes", () => ({
getModeBySlug: vi.fn(),
@ -14,6 +33,33 @@ vi.mock("../../prompts/responses", () => ({
},
}))
vi.mock("../updateTodoListTool", () => ({
parseMarkdownChecklist: vi.fn((md: string) => {
// Simple mock implementation
const lines = md.split("\n").filter((line) => line.trim())
return lines.map((line, index) => {
let status = "pending"
let content = line
if (line.includes("[x]") || line.includes("[X]")) {
status = "completed"
content = line.replace(/^\[x\]\s*/i, "")
} else if (line.includes("[-]") || line.includes("[~]")) {
status = "in_progress"
content = line.replace(/^\[-\]\s*/, "").replace(/^\[~\]\s*/, "")
} else {
content = line.replace(/^\[\s*\]\s*/, "")
}
return {
id: `todo-${index}`,
content,
status,
}
})
}),
}))
// Define a minimal type for the resolved value
type MockClineInstance = { taskId: string }
@ -22,7 +68,9 @@ const mockAskApproval = vi.fn<AskApproval>()
const mockHandleError = vi.fn<HandleError>()
const mockPushToolResult = vi.fn()
const mockRemoveClosingTag = vi.fn((_name: string, value: string | undefined) => value ?? "")
const mockCreateTask = vi.fn<() => Promise<MockClineInstance>>().mockResolvedValue({ taskId: "mock-subtask-id" })
const mockCreateTask = vi
.fn<(text?: string, images?: string[], parentTask?: any, options?: any) => Promise<MockClineInstance>>()
.mockResolvedValue({ taskId: "mock-subtask-id" })
const mockEmit = vi.fn()
const mockRecordToolError = vi.fn()
const mockSayAndCreateMissingParamError = vi.fn()
@ -49,6 +97,7 @@ const mockCline = {
import { newTaskTool } from "../newTaskTool"
import type { ToolUse } from "../../../shared/tools"
import { getModeBySlug } from "../../../shared/modes"
import * as vscode from "vscode"
describe("newTaskTool", () => {
beforeEach(() => {
@ -63,6 +112,11 @@ describe("newTaskTool", () => {
}) // Default valid mode
mockCline.consecutiveMistakeCount = 0
mockCline.isPaused = false
// Default: VSCode setting is disabled
const mockGet = vi.fn().mockReturnValue(false)
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
get: mockGet,
} as any)
})
it("should correctly un-escape \\\\@ to \\@ in the message passed to the new task", async () => {
@ -72,6 +126,7 @@ describe("newTaskTool", () => {
params: {
mode: "code",
message: "Review this: \\\\@file1.txt and also \\\\\\\\@file2.txt", // Input with \\@ and \\\\@
todos: "[ ] First task\n[ ] Second task",
},
partial: false,
}
@ -93,6 +148,12 @@ describe("newTaskTool", () => {
"Review this: \\@file1.txt and also \\\\\\@file2.txt", // Unit Test Expectation: \\@ -> \@, \\\\@ -> \\\\@
undefined,
mockCline,
expect.objectContaining({
initialTodos: expect.arrayContaining([
expect.objectContaining({ content: "First task" }),
expect.objectContaining({ content: "Second task" }),
]),
}),
)
// Verify side effects
@ -109,6 +170,7 @@ describe("newTaskTool", () => {
params: {
mode: "code",
message: "This is already unescaped: \\@file1.txt",
todos: "[ ] Test todo",
},
partial: false,
}
@ -126,6 +188,9 @@ describe("newTaskTool", () => {
"This is already unescaped: \\@file1.txt", // Expected: \@ remains \@
undefined,
mockCline,
expect.objectContaining({
initialTodos: expect.any(Array),
}),
)
})
@ -136,6 +201,7 @@ describe("newTaskTool", () => {
params: {
mode: "code",
message: "A normal mention @file1.txt",
todos: "[ ] Test todo",
},
partial: false,
}
@ -153,6 +219,9 @@ describe("newTaskTool", () => {
"A normal mention @file1.txt", // Expected: @ remains @
undefined,
mockCline,
expect.objectContaining({
initialTodos: expect.any(Array),
}),
)
})
@ -163,6 +232,7 @@ describe("newTaskTool", () => {
params: {
mode: "code",
message: "Mix: @file0.txt, \\@file1.txt, \\\\@file2.txt, \\\\\\\\@file3.txt",
todos: "[ ] Test todo",
},
partial: false,
}
@ -180,8 +250,422 @@ describe("newTaskTool", () => {
"Mix: @file0.txt, \\@file1.txt, \\@file2.txt, \\\\\\@file3.txt", // Unit Test Expectation: @->@, \@->\@, \\@->\@, \\\\@->\\\\@
undefined,
mockCline,
expect.objectContaining({
initialTodos: expect.any(Array),
}),
)
})
// Add more tests for error handling (missing params, invalid mode, approval denied) if needed
it("should handle missing todos parameter gracefully (backward compatibility)", async () => {
const block: ToolUse = {
type: "tool_use",
name: "new_task",
params: {
mode: "code",
message: "Test message",
// todos missing - should work for backward compatibility
},
partial: false,
}
await newTaskTool(
mockCline as any,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Should NOT error when todos is missing
expect(mockSayAndCreateMissingParamError).not.toHaveBeenCalledWith("new_task", "todos")
expect(mockCline.consecutiveMistakeCount).toBe(0)
expect(mockCline.recordToolError).not.toHaveBeenCalledWith("new_task")
// Should create task with empty todos array
expect(mockCreateTask).toHaveBeenCalledWith(
"Test message",
undefined,
mockCline,
expect.objectContaining({
initialTodos: [],
}),
)
// Should complete successfully
expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining("Successfully created new task"))
})
it("should work with todos parameter when provided", async () => {
const block: ToolUse = {
type: "tool_use",
name: "new_task",
params: {
mode: "code",
message: "Test message with todos",
todos: "[ ] First task\n[ ] Second task",
},
partial: false,
}
await newTaskTool(
mockCline as any,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Should parse and include todos when provided
expect(mockCreateTask).toHaveBeenCalledWith(
"Test message with todos",
undefined,
mockCline,
expect.objectContaining({
initialTodos: expect.arrayContaining([
expect.objectContaining({ content: "First task" }),
expect.objectContaining({ content: "Second task" }),
]),
}),
)
expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining("Successfully created new task"))
})
it("should error when mode parameter is missing", async () => {
const block: ToolUse = {
type: "tool_use",
name: "new_task",
params: {
// mode missing
message: "Test message",
todos: "[ ] Test todo",
},
partial: false,
}
await newTaskTool(
mockCline as any,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
expect(mockSayAndCreateMissingParamError).toHaveBeenCalledWith("new_task", "mode")
expect(mockCline.consecutiveMistakeCount).toBe(1)
expect(mockCline.recordToolError).toHaveBeenCalledWith("new_task")
})
it("should error when message parameter is missing", async () => {
const block: ToolUse = {
type: "tool_use",
name: "new_task",
params: {
mode: "code",
// message missing
todos: "[ ] Test todo",
},
partial: false,
}
await newTaskTool(
mockCline as any,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
expect(mockSayAndCreateMissingParamError).toHaveBeenCalledWith("new_task", "message")
expect(mockCline.consecutiveMistakeCount).toBe(1)
expect(mockCline.recordToolError).toHaveBeenCalledWith("new_task")
})
it("should parse todos with different statuses correctly", async () => {
const block: ToolUse = {
type: "tool_use",
name: "new_task",
params: {
mode: "code",
message: "Test message",
todos: "[ ] Pending task\n[x] Completed task\n[-] In progress task",
},
partial: false,
}
await newTaskTool(
mockCline as any,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
expect(mockCreateTask).toHaveBeenCalledWith(
"Test message",
undefined,
mockCline,
expect.objectContaining({
initialTodos: expect.arrayContaining([
expect.objectContaining({ content: "Pending task", status: "pending" }),
expect.objectContaining({ content: "Completed task", status: "completed" }),
expect.objectContaining({ content: "In progress task", status: "in_progress" }),
]),
}),
)
})
describe("VSCode setting: newTaskRequireTodos", () => {
it("should NOT require todos when VSCode setting is disabled (default)", async () => {
// Ensure VSCode setting is disabled
const mockGet = vi.fn().mockReturnValue(false)
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
get: mockGet,
} as any)
const block: ToolUse = {
type: "tool_use",
name: "new_task",
params: {
mode: "code",
message: "Test message",
// todos missing - should work when setting is disabled
},
partial: false,
}
await newTaskTool(
mockCline as any,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Should NOT error when todos is missing and setting is disabled
expect(mockSayAndCreateMissingParamError).not.toHaveBeenCalledWith("new_task", "todos")
expect(mockCline.consecutiveMistakeCount).toBe(0)
expect(mockCline.recordToolError).not.toHaveBeenCalledWith("new_task")
// Should create task with empty todos array
expect(mockCreateTask).toHaveBeenCalledWith(
"Test message",
undefined,
mockCline,
expect.objectContaining({
initialTodos: [],
}),
)
// Should complete successfully
expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining("Successfully created new task"))
})
it("should REQUIRE todos when VSCode setting is enabled", async () => {
// Enable VSCode setting
const mockGet = vi.fn().mockReturnValue(true)
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
get: mockGet,
} as any)
const block: ToolUse = {
type: "tool_use",
name: "new_task",
params: {
mode: "code",
message: "Test message",
// todos missing - should error when setting is enabled
},
partial: false,
}
await newTaskTool(
mockCline as any,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Should error when todos is missing and setting is enabled
expect(mockSayAndCreateMissingParamError).toHaveBeenCalledWith("new_task", "todos")
expect(mockCline.consecutiveMistakeCount).toBe(1)
expect(mockCline.recordToolError).toHaveBeenCalledWith("new_task")
// Should NOT create task
expect(mockCreateTask).not.toHaveBeenCalled()
expect(mockPushToolResult).not.toHaveBeenCalledWith(
expect.stringContaining("Successfully created new task"),
)
})
it("should work with todos when VSCode setting is enabled", async () => {
// Enable VSCode setting
const mockGet = vi.fn().mockReturnValue(true)
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
get: mockGet,
} as any)
const block: ToolUse = {
type: "tool_use",
name: "new_task",
params: {
mode: "code",
message: "Test message",
todos: "[ ] First task\n[ ] Second task",
},
partial: false,
}
await newTaskTool(
mockCline as any,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Should NOT error when todos is provided and setting is enabled
expect(mockSayAndCreateMissingParamError).not.toHaveBeenCalledWith("new_task", "todos")
expect(mockCline.consecutiveMistakeCount).toBe(0)
// Should create task with parsed todos
expect(mockCreateTask).toHaveBeenCalledWith(
"Test message",
undefined,
mockCline,
expect.objectContaining({
initialTodos: expect.arrayContaining([
expect.objectContaining({ content: "First task" }),
expect.objectContaining({ content: "Second task" }),
]),
}),
)
// Should complete successfully
expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining("Successfully created new task"))
})
it("should work with empty todos string when VSCode setting is enabled", async () => {
// Enable VSCode setting
const mockGet = vi.fn().mockReturnValue(true)
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
get: mockGet,
} as any)
const block: ToolUse = {
type: "tool_use",
name: "new_task",
params: {
mode: "code",
message: "Test message",
todos: "", // Empty string should be accepted
},
partial: false,
}
await newTaskTool(
mockCline as any,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Should NOT error when todos is empty string and setting is enabled
expect(mockSayAndCreateMissingParamError).not.toHaveBeenCalledWith("new_task", "todos")
expect(mockCline.consecutiveMistakeCount).toBe(0)
// Should create task with empty todos array
expect(mockCreateTask).toHaveBeenCalledWith(
"Test message",
undefined,
mockCline,
expect.objectContaining({
initialTodos: [],
}),
)
// Should complete successfully
expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining("Successfully created new task"))
})
it("should check VSCode setting with Package.name configuration key", async () => {
const mockGet = vi.fn().mockReturnValue(false)
const mockGetConfiguration = vi.fn().mockReturnValue({
get: mockGet,
} as any)
vi.mocked(vscode.workspace.getConfiguration).mockImplementation(mockGetConfiguration)
const block: ToolUse = {
type: "tool_use",
name: "new_task",
params: {
mode: "code",
message: "Test message",
},
partial: false,
}
await newTaskTool(
mockCline as any,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Verify that VSCode configuration was accessed with Package.name
expect(mockGetConfiguration).toHaveBeenCalledWith("roo-cline")
expect(mockGet).toHaveBeenCalledWith("newTaskRequireTodos", false)
})
it("should use current Package.name value (roo-code-nightly) when accessing VSCode configuration", async () => {
// Arrange: capture calls to VSCode configuration and ensure we can assert the namespace
const mockGet = vi.fn().mockReturnValue(false)
const mockGetConfiguration = vi.fn().mockReturnValue({
get: mockGet,
} as any)
vi.mocked(vscode.workspace.getConfiguration).mockImplementation(mockGetConfiguration)
// Mutate the mocked Package.name dynamically to simulate a different build variant
const pkg = await import("../../../shared/package")
;(pkg.Package as any).name = "roo-code-nightly"
const block: ToolUse = {
type: "tool_use",
name: "new_task",
params: {
mode: "code",
message: "Test message",
},
partial: false,
}
await newTaskTool(
mockCline as any,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Assert: configuration was read using the dynamic nightly namespace
expect(mockGetConfiguration).toHaveBeenCalledWith("roo-code-nightly")
expect(mockGet).toHaveBeenCalledWith("newTaskRequireTodos", false)
})
})
// Add more tests for error handling (invalid mode, approval denied) if needed
})

View file

@ -1,12 +1,15 @@
import delay from "delay"
import * as vscode from "vscode"
import { RooCodeEventName } from "@roo-code/types"
import { RooCodeEventName, TodoItem } from "@roo-code/types"
import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
import { Task } from "../task/Task"
import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
import { formatResponse } from "../prompts/responses"
import { t } from "../../i18n"
import { parseMarkdownChecklist } from "./updateTodoListTool"
import { Package } from "../../shared/package"
export async function newTaskTool(
cline: Task,
@ -18,6 +21,7 @@ export async function newTaskTool(
) {
const mode: string | undefined = block.params.mode
const message: string | undefined = block.params.message
const todos: string | undefined = block.params.todos
try {
if (block.partial) {
@ -25,11 +29,13 @@ export async function newTaskTool(
tool: "newTask",
mode: removeClosingTag("mode", mode),
content: removeClosingTag("message", message),
todos: removeClosingTag("todos", todos),
})
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
return
} else {
// Validate required parameters
if (!mode) {
cline.consecutiveMistakeCount++
cline.recordToolError("new_task")
@ -44,13 +50,49 @@ export async function newTaskTool(
return
}
// Get the VSCode setting for requiring todos
const provider = cline.providerRef.deref()
if (!provider) {
pushToolResult(formatResponse.toolError("Provider reference lost"))
return
}
const state = await provider.getState()
// Use Package.name (dynamic at build time) as the VSCode configuration namespace.
// Supports multiple extension variants (e.g., stable/nightly) without hardcoded strings.
const requireTodos = vscode.workspace
.getConfiguration(Package.name)
.get<boolean>("newTaskRequireTodos", false)
// Check if todos are required based on VSCode setting
// Note: undefined means not provided, empty string is valid
if (requireTodos && todos === undefined) {
cline.consecutiveMistakeCount++
cline.recordToolError("new_task")
pushToolResult(await cline.sayAndCreateMissingParamError("new_task", "todos"))
return
}
// Parse todos if provided, otherwise use empty array
let todoItems: TodoItem[] = []
if (todos) {
try {
todoItems = parseMarkdownChecklist(todos)
} catch (error) {
cline.consecutiveMistakeCount++
cline.recordToolError("new_task")
pushToolResult(formatResponse.toolError("Invalid todos format: must be a markdown checklist"))
return
}
}
cline.consecutiveMistakeCount = 0
// Un-escape one level of backslashes before '@' for hierarchical subtasks
// Un-escape one level: \\@ -> \@ (removes one backslash for hierarchical subtasks)
const unescapedMessage = message.replace(/\\\\@/g, "\\@")
// Verify the mode exists
const targetMode = getModeBySlug(mode, (await cline.providerRef.deref()?.getState())?.customModes)
const targetMode = getModeBySlug(mode, state?.customModes)
if (!targetMode) {
pushToolResult(formatResponse.toolError(`Invalid mode: ${mode}`))
@ -61,6 +103,7 @@ export async function newTaskTool(
tool: "newTask",
mode: targetMode.name,
content: message,
todos: todoItems,
})
const didApprove = await askApproval("tool", toolMessage)
@ -69,11 +112,7 @@ export async function newTaskTool(
return
}
const provider = cline.providerRef.deref()
if (!provider) {
return
}
// Provider is guaranteed to be defined here due to earlier check
if (cline.enableCheckpoints) {
cline.checkpointSave(true)
@ -83,8 +122,9 @@ export async function newTaskTool(
cline.pausedModeSlug = (await provider.getState()).mode ?? defaultModeSlug
// Create new task instance first (this preserves parent's current mode in its history)
const newCline = await provider.createTask(unescapedMessage, undefined, cline)
const newCline = await provider.createTask(unescapedMessage, undefined, cline, {
initialTodos: todoItems,
})
if (!newCline) {
pushToolResult(t("tools:newTask.errors.policy_restriction"))
return
@ -98,7 +138,9 @@ export async function newTaskTool(
cline.emit(RooCodeEventName.TaskSpawned, newCline.taskId)
pushToolResult(`Successfully created new task in ${targetMode.name} mode with message: ${unescapedMessage}`)
pushToolResult(
`Successfully created new task in ${targetMode.name} mode with message: ${unescapedMessage} and ${todoItems.length} todo items`,
)
// Set the isPaused flag to true so the parent
// task can wait for the sub-task to finish.

View file

@ -100,7 +100,7 @@ function normalizeStatus(status: string | undefined): TodoStatus {
return "pending"
}
function parseMarkdownChecklist(md: string): TodoItem[] {
export function parseMarkdownChecklist(md: string): TodoItem[] {
if (typeof md !== "string") return []
const lines = md
.split(/\r?\n/)

View file

@ -806,6 +806,7 @@ export class ClineProvider
taskNumber: this.clineStack.length + 1,
onCreated: this.taskCreationCallback,
enableTaskBridge: isRemoteControlEnabled(cloudUserInfo, remoteControlEnabled),
initialTodos: options.initialTodos,
...options,
})
@ -1568,7 +1569,8 @@ export class ClineProvider
this.postMessageToWebview({ type: "state", state })
// Check MDM compliance and send user to account tab if not compliant
if (!this.checkMdmCompliance()) {
// Only redirect if there's an actual MDM policy requiring authentication
if (this.mdmService?.requiresCloudAuth() && !this.checkMdmCompliance()) {
await this.postMessageToWebview({ type: "action", action: "accountButtonClicked" })
}
}
@ -1805,6 +1807,7 @@ export class ClineProvider
? (taskHistory || []).find((item: HistoryItem) => item.id === this.getCurrentTask()?.taskId)
: undefined,
clineMessages: this.getCurrentTask()?.clineMessages || [],
currentTaskTodos: this.getCurrentTask()?.todoList || [],
taskHistory: (taskHistory || [])
.filter((item: HistoryItem) => item.ts && item.task)
.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
@ -1887,7 +1890,9 @@ export class ClineProvider
codebaseIndexSearchMaxResults: codebaseIndexConfig?.codebaseIndexSearchMaxResults,
codebaseIndexSearchMinScore: codebaseIndexConfig?.codebaseIndexSearchMinScore,
},
mdmCompliant: this.checkMdmCompliance(),
// Only set mdmCompliant if there's an actual MDM policy
// undefined means no MDM policy, true means compliant, false means non-compliant
mdmCompliant: this.mdmService?.requiresCloudAuth() ? this.checkMdmCompliance() : undefined,
profileThresholds: profileThresholds ?? {},
cloudApiUrl: getRooCodeApiUrl(),
hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false,
@ -2187,7 +2192,7 @@ export class ClineProvider
/**
* Check if the current state is compliant with MDM policy
* @returns true if compliant, false if blocked
* @returns true if compliant or no MDM policy exists, false if MDM policy exists and user is non-compliant
*/
public checkMdmCompliance(): boolean {
if (!this.mdmService) {

View file

@ -85,6 +85,9 @@ export const generateSystemPrompt = async (provider: ClineProvider, message: Web
maxConcurrentFileReads: maxConcurrentFileReads ?? 5,
todoListEnabled: apiConfiguration?.todoListEnabled ?? true,
useAgentRules: vscode.workspace.getConfiguration("roo-cline").get<boolean>("useAgentRules") ?? true,
newTaskRequireTodos: vscode.workspace
.getConfiguration("roo-cline")
.get<boolean>("newTaskRequireTodos", false),
},
)

View file

@ -288,7 +288,24 @@ export const webviewMessageHandler = async (
// Initializing new instance of Cline will make sure that any
// agentically running promises in old instance don't affect our new
// task. This essentially creates a fresh slate for the new task.
await provider.createTask(message.text, message.images)
try {
await provider.createTask(message.text, message.images)
// Task created successfully - notify the UI to reset
await provider.postMessageToWebview({
type: "invoke",
invoke: "newChat",
})
} catch (error) {
// For all errors, reset the UI and show error
await provider.postMessageToWebview({
type: "invoke",
invoke: "newChat",
})
// Show error to user
vscode.window.showErrorMessage(
`Failed to create task: ${error instanceof Error ? error.message : String(error)}`,
)
}
break
case "customInstructions":
await provider.updateCustomInstructions(message.text)
@ -548,7 +565,14 @@ export const webviewMessageHandler = async (
const modelFetchPromises: Array<{ key: RouterName; options: GetModelsOptions }> = [
{ key: "openrouter", options: { provider: "openrouter" } },
{ key: "requesty", options: { provider: "requesty", apiKey: apiConfiguration.requestyApiKey } },
{
key: "requesty",
options: {
provider: "requesty",
apiKey: apiConfiguration.requestyApiKey,
baseUrl: apiConfiguration.requestyBaseUrl,
},
},
{ key: "glama", options: { provider: "glama" } },
{ key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } },
]
@ -2618,5 +2642,10 @@ export const webviewMessageHandler = async (
}
break
}
case "showMdmAuthRequiredNotification": {
// Show notification that organization requires authentication
vscode.window.showWarningMessage(t("common:mdm.info.organization_requires_auth"))
break
}
}
}

View file

@ -191,6 +191,9 @@
"cloud_auth_required": "La teva organització requereix autenticació de Roo Code Cloud. Si us plau, inicia sessió per continuar.",
"organization_mismatch": "Has d'estar autenticat amb el compte de Roo Code Cloud de la teva organització.",
"verification_failed": "No s'ha pogut verificar l'autenticació de l'organització."
},
"info": {
"organization_requires_auth": "La teva organització requereix autenticació."
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "No s'han trobat etiquetes.",
"selected": "Mostrant elements amb qualsevol de les etiquetes seleccionades"
},
"installed": {
"label": "Filtra per estat",
"all": "Tots els articles",
"installed": "Instal·lats",
"notInstalled": "No instal·lats"
},
"title": "Marketplace"
},
"done": "Fet",

View file

@ -191,6 +191,9 @@
"cloud_auth_required": "Deine Organisation erfordert eine Roo Code Cloud-Authentifizierung. Bitte melde dich an, um fortzufahren.",
"organization_mismatch": "Du musst mit dem Roo Code Cloud-Konto deiner Organisation authentifiziert sein.",
"verification_failed": "Die Organisationsauthentifizierung konnte nicht verifiziert werden."
},
"info": {
"organization_requires_auth": "Deine Organisation erfordert eine Authentifizierung."
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "Keine Tags gefunden.",
"selected": "Zeige Elemente mit einem der ausgewählten Tags"
},
"installed": {
"label": "Nach Status filtern",
"all": "Alle Artikel",
"installed": "Installierte",
"notInstalled": "Nicht installiert"
},
"title": "Marketplace"
},
"done": "Fertig",

View file

@ -180,6 +180,9 @@
"cloud_auth_required": "Your organization requires Roo Code Cloud authentication. Please sign in to continue.",
"organization_mismatch": "You must be authenticated with your organization's Roo Code Cloud account.",
"verification_failed": "Unable to verify organization authentication."
},
"info": {
"organization_requires_auth": "Your organization requires authentication."
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "No tags found.",
"selected": "Showing items with any of the selected tags"
},
"installed": {
"label": "Filter by status",
"all": "All Items",
"installed": "Installed",
"notInstalled": "Not Installed"
},
"title": "Marketplace"
},
"done": "Done",

View file

@ -191,6 +191,9 @@
"cloud_auth_required": "Tu organización requiere autenticación de Roo Code Cloud. Por favor, inicia sesión para continuar.",
"organization_mismatch": "Debes estar autenticado con la cuenta de Roo Code Cloud de tu organización.",
"verification_failed": "No se pudo verificar la autenticación de la organización."
},
"info": {
"organization_requires_auth": "Tu organización requiere autenticación."
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "No se encontraron etiquetas.",
"selected": "Mostrando elementos con cualquiera de las etiquetas seleccionadas"
},
"installed": {
"label": "Filtrar por estado",
"all": "Todos los artículos",
"installed": "Instalados",
"notInstalled": "No instalados"
},
"title": "Marketplace"
},
"done": "Hecho",

View file

@ -191,6 +191,9 @@
"cloud_auth_required": "Votre organisation nécessite une authentification Roo Code Cloud. Veuillez vous connecter pour continuer.",
"organization_mismatch": "Vous devez être authentifié avec le compte Roo Code Cloud de votre organisation.",
"verification_failed": "Impossible de vérifier l'authentification de l'organisation."
},
"info": {
"organization_requires_auth": "Votre organisation nécessite une authentification."
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "Aucune étiquette trouvée.",
"selected": "Affichage des éléments avec l'une des étiquettes sélectionnées"
},
"installed": {
"label": "Filtrer par statut",
"all": "Tous les articles",
"installed": "Installés",
"notInstalled": "Non installés"
},
"title": "Marketplace"
},
"done": "Terminé",

View file

@ -191,6 +191,9 @@
"cloud_auth_required": "आपके संगठन को Roo Code Cloud प्रमाणीकरण की आवश्यकता है। कृपया जारी रखने के लिए साइन इन करें।",
"organization_mismatch": "आपको अपने संगठन के Roo Code Cloud खाते से प्रमाणित होना होगा।",
"verification_failed": "संगठन प्रमाणीकरण सत्यापित करने में असमर्थ।"
},
"info": {
"organization_requires_auth": "आपके संगठन को प्रमाणीकरण की आवश्यकता है।"
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "कोई टैग नहीं मिले।",
"selected": "चयनित टैग्स में से किसी भी के साथ आइटम दिखा रहे हैं"
},
"installed": {
"label": "स्थिति के अनुसार फ़िल्टर करें",
"all": "सभी आइटम",
"installed": "स्थापित",
"notInstalled": "स्थापित नहीं"
},
"title": "मार्केटप्लेस"
},
"done": "हो गया",

View file

@ -191,6 +191,9 @@
"cloud_auth_required": "Organisasi kamu memerlukan autentikasi Roo Code Cloud. Silakan masuk untuk melanjutkan.",
"organization_mismatch": "Kamu harus diautentikasi dengan akun Roo Code Cloud organisasi kamu.",
"verification_failed": "Tidak dapat memverifikasi autentikasi organisasi."
},
"info": {
"organization_requires_auth": "Organisasi kamu memerlukan autentikasi."
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "Tidak ada tag ditemukan.",
"selected": "Menampilkan item dengan salah satu tag yang dipilih"
},
"installed": {
"label": "Filter berdasarkan status",
"all": "Semua Item",
"installed": "Terpasang",
"notInstalled": "Tidak Terpasang"
},
"title": "Marketplace"
},
"done": "Selesai",

View file

@ -191,6 +191,9 @@
"cloud_auth_required": "La tua organizzazione richiede l'autenticazione Roo Code Cloud. Accedi per continuare.",
"organization_mismatch": "Devi essere autenticato con l'account Roo Code Cloud della tua organizzazione.",
"verification_failed": "Impossibile verificare l'autenticazione dell'organizzazione."
},
"info": {
"organization_requires_auth": "La tua organizzazione richiede l'autenticazione."
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "Nessun tag trovato.",
"selected": "Mostrando elementi con uno qualsiasi dei tag selezionati"
},
"installed": {
"label": "Filtra per stato",
"all": "Tutti gli articoli",
"installed": "Installati",
"notInstalled": "Non installati"
},
"title": "Marketplace"
},
"done": "Fatto",

View file

@ -191,6 +191,9 @@
"cloud_auth_required": "あなたの組織では Roo Code Cloud 認証が必要です。続行するにはサインインしてください。",
"organization_mismatch": "組織の Roo Code Cloud アカウントで認証する必要があります。",
"verification_failed": "組織認証の確認ができませんでした。"
},
"info": {
"organization_requires_auth": "あなたの組織では認証が必要です。"
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "タグが見つかりません。",
"selected": "選択されたタグのいずれかを持つアイテムを表示"
},
"installed": {
"label": "ステータスで絞り込む",
"all": "すべてのアイテム",
"installed": "インストール済み",
"notInstalled": "未インストール"
},
"title": "マーケットプレイス"
},
"done": "完了",

View file

@ -191,6 +191,9 @@
"cloud_auth_required": "조직에서 Roo Code Cloud 인증이 필요합니다. 계속하려면 로그인하세요.",
"organization_mismatch": "조직의 Roo Code Cloud 계정으로 인증해야 합니다.",
"verification_failed": "조직 인증을 확인할 수 없습니다."
},
"info": {
"organization_requires_auth": "조직에서 인증이 필요합니다."
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "태그를 찾을 수 없습니다.",
"selected": "선택된 태그 중 하나를 가진 항목 표시"
},
"installed": {
"label": "상태별로 필터링",
"all": "모든 항목",
"installed": "설치됨",
"notInstalled": "설치되지 않음"
},
"title": "마켓플레이스"
},
"done": "완료",

View file

@ -191,6 +191,9 @@
"cloud_auth_required": "Je organisatie vereist Roo Code Cloud-authenticatie. Log in om door te gaan.",
"organization_mismatch": "Je moet geauthenticeerd zijn met het Roo Code Cloud-account van je organisatie.",
"verification_failed": "Kan organisatie-authenticatie niet verifiëren."
},
"info": {
"organization_requires_auth": "Je organisatie vereist authenticatie."
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "Geen tags gevonden.",
"selected": "Items tonen met een van de geselecteerde tags"
},
"installed": {
"label": "Filteren op status",
"all": "Alle items",
"installed": "Geïnstalleerd",
"notInstalled": "Niet geïnstalleerd"
},
"title": "Marketplace"
},
"done": "Klaar",

View file

@ -191,6 +191,9 @@
"cloud_auth_required": "Twoja organizacja wymaga uwierzytelnienia Roo Code Cloud. Zaloguj się, aby kontynuować.",
"organization_mismatch": "Musisz być uwierzytelniony kontem Roo Code Cloud swojej organizacji.",
"verification_failed": "Nie można zweryfikować uwierzytelnienia organizacji."
},
"info": {
"organization_requires_auth": "Twoja organizacja wymaga uwierzytelnienia."
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "Nie znaleziono tagów.",
"selected": "Pokazywanie elementów z dowolnym z wybranych tagów"
},
"installed": {
"label": "Filtruj według statusu",
"all": "Wszystkie elementy",
"installed": "Zainstalowane",
"notInstalled": "Niezainstalowane"
},
"title": "Marketplace"
},
"done": "Gotowe",

View file

@ -191,6 +191,9 @@
"cloud_auth_required": "Sua organização requer autenticação do Roo Code Cloud. Faça login para continuar.",
"organization_mismatch": "Você deve estar autenticado com a conta Roo Code Cloud da sua organização.",
"verification_failed": "Não foi possível verificar a autenticação da organização."
},
"info": {
"organization_requires_auth": "Sua organização requer autenticação."
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "Nenhuma tag encontrada.",
"selected": "Mostrando itens com qualquer uma das tags selecionadas"
},
"installed": {
"label": "Filtrar por status",
"all": "Todos os itens",
"installed": "Instalados",
"notInstalled": "Não instalados"
},
"title": "Marketplace"
},
"done": "Concluído",

View file

@ -191,6 +191,9 @@
"cloud_auth_required": "Ваша организация требует аутентификации Roo Code Cloud. Войдите в систему, чтобы продолжить.",
"organization_mismatch": "Вы должны быть аутентифицированы с учетной записью Roo Code Cloud вашей организации.",
"verification_failed": "Не удается проверить аутентификацию организации."
},
"info": {
"organization_requires_auth": "Ваша организация требует аутентификации."
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "Теги не найдены.",
"selected": "Показ элементов с любым из выбранных тегов"
},
"installed": {
"label": "Фильтр по статусу",
"all": "Все элементы",
"installed": "Установленные",
"notInstalled": "Не установленные"
},
"title": "Marketplace"
},
"done": "Готово",

View file

@ -191,6 +191,9 @@
"cloud_auth_required": "Kuruluşunuz Roo Code Cloud kimlik doğrulaması gerektiriyor. Devam etmek için giriş yapın.",
"organization_mismatch": "Kuruluşunuzun Roo Code Cloud hesabıyla kimlik doğrulaması yapmalısınız.",
"verification_failed": "Kuruluş kimlik doğrulaması doğrulanamıyor."
},
"info": {
"organization_requires_auth": "Kuruluşunuz kimlik doğrulaması gerektiriyor."
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "Etiket bulunamadı.",
"selected": "Seçilen etiketlerden herhangi birine sahip öğeleri göster"
},
"installed": {
"label": "Duruma göre filtrele",
"all": "Tüm Öğeler",
"installed": "Yüklü",
"notInstalled": "Yüklü Değil"
},
"title": "Marketplace"
},
"done": "Tamam",

View file

@ -191,6 +191,9 @@
"cloud_auth_required": "Tổ chức của bạn yêu cầu xác thực Roo Code Cloud. Vui lòng đăng nhập để tiếp tục.",
"organization_mismatch": "Bạn phải được xác thực bằng tài khoản Roo Code Cloud của tổ chức.",
"verification_failed": "Không thể xác minh xác thực tổ chức."
},
"info": {
"organization_requires_auth": "Tổ chức của bạn yêu cầu xác thực."
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "Không tìm thấy thẻ nào.",
"selected": "Hiển thị các mục có bất kỳ thẻ nào được chọn"
},
"installed": {
"label": "Lọc theo trạng thái",
"all": "Tất cả các mục",
"installed": "Đã cài đặt",
"notInstalled": "Chưa cài đặt"
},
"title": "Marketplace"
},
"done": "Hoàn thành",

View file

@ -196,6 +196,9 @@
"cloud_auth_required": "您的组织需要 Roo Code Cloud 身份验证。请登录以继续。",
"organization_mismatch": "您必须使用组织的 Roo Code Cloud 账户进行身份验证。",
"verification_failed": "无法验证组织身份验证。"
},
"info": {
"organization_requires_auth": "您的组织需要身份验证。"
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "未找到标签。",
"selected": "显示包含任一选中标签的项目"
},
"installed": {
"label": "按状态筛选",
"all": "所有项目",
"installed": "已安装",
"notInstalled": "未安装"
},
"title": "Marketplace"
},
"done": "完成",

View file

@ -191,6 +191,9 @@
"cloud_auth_required": "您的組織需要 Roo Code Cloud 身份驗證。請登入以繼續。",
"organization_mismatch": "您必須使用組織的 Roo Code Cloud 帳戶進行身份驗證。",
"verification_failed": "無法驗證組織身份驗證。"
},
"info": {
"organization_requires_auth": "您的組織需要身份驗證。"
}
},
"prompts": {

View file

@ -38,6 +38,12 @@
"noResults": "找不到標籤。",
"selected": "顯示包含任一選取標籤的項目"
},
"installed": {
"label": "按狀態篩選",
"all": "所有項目",
"installed": "已安裝",
"notInstalled": "未安裝"
},
"title": "Marketplace"
},
"done": "完成",

View file

@ -3,7 +3,7 @@
"displayName": "%extension.displayName%",
"description": "%extension.description%",
"publisher": "RooVeterinaryInc",
"version": "3.25.20",
"version": "3.25.23",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
@ -398,6 +398,11 @@
"minimum": 0,
"maximum": 3600,
"description": "%settings.apiRequestTimeout.description%"
},
"roo-cline.newTaskRequireTodos": {
"type": "boolean",
"default": false,
"description": "%settings.newTaskRequireTodos.description%"
}
}
}
@ -424,7 +429,7 @@
"@aws-sdk/credential-providers": "^3.848.0",
"@google/genai": "^1.0.0",
"@lmstudio/sdk": "^1.1.1",
"@mistralai/mistralai": "^1.3.6",
"@mistralai/mistralai": "^1.9.18",
"@modelcontextprotocol/sdk": "^1.9.0",
"@qdrant/js-client-rest": "^1.14.0",
"@roo-code/cloud": "^0.19.0",

View file

@ -39,5 +39,6 @@
"settings.enableCodeActions.description": "Enable Roo Code quick fixes",
"settings.autoImportSettingsPath.description": "Path to a RooCode configuration file to automatically import on extension startup. Supports absolute paths and paths relative to the home directory (e.g. '~/Documents/roo-code-settings.json'). Leave empty to disable auto-import.",
"settings.useAgentRules.description": "Enable loading of AGENTS.md files for agent-specific rules (see https://agent-rules.org/)",
"settings.apiRequestTimeout.description": "Maximum time in seconds to wait for API responses (0 = no timeout, 1-3600s, default: 600s). Higher values are recommended for local providers like LM Studio and Ollama that may need more processing time."
"settings.apiRequestTimeout.description": "Maximum time in seconds to wait for API responses (0 = no timeout, 1-3600s, default: 600s). Higher values are recommended for local providers like LM Studio and Ollama that may need more processing time.",
"settings.newTaskRequireTodos.description": "Require todos parameter when creating new tasks with the new_task tool"
}

Some files were not shown because too many files have changed in this diff Show more