mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Merge branch 'main' into will/mode-plumbing
This commit is contained in:
commit
bfd67cc935
183 changed files with 3806 additions and 673 deletions
5
.gitattributes
vendored
5
.gitattributes
vendored
|
|
@ -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
|
||||
|
||||
|
|
|
|||
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
DATABASE_URL=postgres://postgres:password@localhost:5432/evals_development
|
||||
DATABASE_URL=postgres://postgres:password@localhost:5433/evals_development
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
446
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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" },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 || ""
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
114
src/core/context/context-management/context-error-handling.ts
Normal file
114
src/core/context/context-management/context-error-handling.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.`
|
||||
}
|
||||
|
|
|
|||
128
src/core/prompts/tools/__tests__/new-task.spec.ts
Normal file
128
src/core/prompts/tools/__tests__/new-task.spec.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,4 +5,5 @@ export interface SystemPromptSettings {
|
|||
maxConcurrentFileReads: number
|
||||
todoListEnabled: boolean
|
||||
useAgentRules: boolean
|
||||
newTaskRequireTodos: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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/)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
3
src/i18n/locales/ca/common.json
generated
3
src/i18n/locales/ca/common.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
6
src/i18n/locales/ca/marketplace.json
generated
6
src/i18n/locales/ca/marketplace.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
3
src/i18n/locales/de/common.json
generated
3
src/i18n/locales/de/common.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
6
src/i18n/locales/de/marketplace.json
generated
6
src/i18n/locales/de/marketplace.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
3
src/i18n/locales/es/common.json
generated
3
src/i18n/locales/es/common.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
6
src/i18n/locales/es/marketplace.json
generated
6
src/i18n/locales/es/marketplace.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
3
src/i18n/locales/fr/common.json
generated
3
src/i18n/locales/fr/common.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
6
src/i18n/locales/fr/marketplace.json
generated
6
src/i18n/locales/fr/marketplace.json
generated
|
|
@ -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é",
|
||||
|
|
|
|||
3
src/i18n/locales/hi/common.json
generated
3
src/i18n/locales/hi/common.json
generated
|
|
@ -191,6 +191,9 @@
|
|||
"cloud_auth_required": "आपके संगठन को Roo Code Cloud प्रमाणीकरण की आवश्यकता है। कृपया जारी रखने के लिए साइन इन करें।",
|
||||
"organization_mismatch": "आपको अपने संगठन के Roo Code Cloud खाते से प्रमाणित होना होगा।",
|
||||
"verification_failed": "संगठन प्रमाणीकरण सत्यापित करने में असमर्थ।"
|
||||
},
|
||||
"info": {
|
||||
"organization_requires_auth": "आपके संगठन को प्रमाणीकरण की आवश्यकता है।"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
|
|
|
|||
6
src/i18n/locales/hi/marketplace.json
generated
6
src/i18n/locales/hi/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "कोई टैग नहीं मिले।",
|
||||
"selected": "चयनित टैग्स में से किसी भी के साथ आइटम दिखा रहे हैं"
|
||||
},
|
||||
"installed": {
|
||||
"label": "स्थिति के अनुसार फ़िल्टर करें",
|
||||
"all": "सभी आइटम",
|
||||
"installed": "स्थापित",
|
||||
"notInstalled": "स्थापित नहीं"
|
||||
},
|
||||
"title": "मार्केटप्लेस"
|
||||
},
|
||||
"done": "हो गया",
|
||||
|
|
|
|||
3
src/i18n/locales/id/common.json
generated
3
src/i18n/locales/id/common.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
6
src/i18n/locales/id/marketplace.json
generated
6
src/i18n/locales/id/marketplace.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
3
src/i18n/locales/it/common.json
generated
3
src/i18n/locales/it/common.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
6
src/i18n/locales/it/marketplace.json
generated
6
src/i18n/locales/it/marketplace.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
3
src/i18n/locales/ja/common.json
generated
3
src/i18n/locales/ja/common.json
generated
|
|
@ -191,6 +191,9 @@
|
|||
"cloud_auth_required": "あなたの組織では Roo Code Cloud 認証が必要です。続行するにはサインインしてください。",
|
||||
"organization_mismatch": "組織の Roo Code Cloud アカウントで認証する必要があります。",
|
||||
"verification_failed": "組織認証の確認ができませんでした。"
|
||||
},
|
||||
"info": {
|
||||
"organization_requires_auth": "あなたの組織では認証が必要です。"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
|
|
|
|||
6
src/i18n/locales/ja/marketplace.json
generated
6
src/i18n/locales/ja/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "タグが見つかりません。",
|
||||
"selected": "選択されたタグのいずれかを持つアイテムを表示"
|
||||
},
|
||||
"installed": {
|
||||
"label": "ステータスで絞り込む",
|
||||
"all": "すべてのアイテム",
|
||||
"installed": "インストール済み",
|
||||
"notInstalled": "未インストール"
|
||||
},
|
||||
"title": "マーケットプレイス"
|
||||
},
|
||||
"done": "完了",
|
||||
|
|
|
|||
3
src/i18n/locales/ko/common.json
generated
3
src/i18n/locales/ko/common.json
generated
|
|
@ -191,6 +191,9 @@
|
|||
"cloud_auth_required": "조직에서 Roo Code Cloud 인증이 필요합니다. 계속하려면 로그인하세요.",
|
||||
"organization_mismatch": "조직의 Roo Code Cloud 계정으로 인증해야 합니다.",
|
||||
"verification_failed": "조직 인증을 확인할 수 없습니다."
|
||||
},
|
||||
"info": {
|
||||
"organization_requires_auth": "조직에서 인증이 필요합니다."
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
|
|
|
|||
6
src/i18n/locales/ko/marketplace.json
generated
6
src/i18n/locales/ko/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "태그를 찾을 수 없습니다.",
|
||||
"selected": "선택된 태그 중 하나를 가진 항목 표시"
|
||||
},
|
||||
"installed": {
|
||||
"label": "상태별로 필터링",
|
||||
"all": "모든 항목",
|
||||
"installed": "설치됨",
|
||||
"notInstalled": "설치되지 않음"
|
||||
},
|
||||
"title": "마켓플레이스"
|
||||
},
|
||||
"done": "완료",
|
||||
|
|
|
|||
3
src/i18n/locales/nl/common.json
generated
3
src/i18n/locales/nl/common.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
6
src/i18n/locales/nl/marketplace.json
generated
6
src/i18n/locales/nl/marketplace.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
3
src/i18n/locales/pl/common.json
generated
3
src/i18n/locales/pl/common.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
6
src/i18n/locales/pl/marketplace.json
generated
6
src/i18n/locales/pl/marketplace.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
3
src/i18n/locales/pt-BR/common.json
generated
3
src/i18n/locales/pt-BR/common.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
6
src/i18n/locales/pt-BR/marketplace.json
generated
6
src/i18n/locales/pt-BR/marketplace.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
3
src/i18n/locales/ru/common.json
generated
3
src/i18n/locales/ru/common.json
generated
|
|
@ -191,6 +191,9 @@
|
|||
"cloud_auth_required": "Ваша организация требует аутентификации Roo Code Cloud. Войдите в систему, чтобы продолжить.",
|
||||
"organization_mismatch": "Вы должны быть аутентифицированы с учетной записью Roo Code Cloud вашей организации.",
|
||||
"verification_failed": "Не удается проверить аутентификацию организации."
|
||||
},
|
||||
"info": {
|
||||
"organization_requires_auth": "Ваша организация требует аутентификации."
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
|
|
|
|||
6
src/i18n/locales/ru/marketplace.json
generated
6
src/i18n/locales/ru/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "Теги не найдены.",
|
||||
"selected": "Показ элементов с любым из выбранных тегов"
|
||||
},
|
||||
"installed": {
|
||||
"label": "Фильтр по статусу",
|
||||
"all": "Все элементы",
|
||||
"installed": "Установленные",
|
||||
"notInstalled": "Не установленные"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "Готово",
|
||||
|
|
|
|||
3
src/i18n/locales/tr/common.json
generated
3
src/i18n/locales/tr/common.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
6
src/i18n/locales/tr/marketplace.json
generated
6
src/i18n/locales/tr/marketplace.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
3
src/i18n/locales/vi/common.json
generated
3
src/i18n/locales/vi/common.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
6
src/i18n/locales/vi/marketplace.json
generated
6
src/i18n/locales/vi/marketplace.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
3
src/i18n/locales/zh-CN/common.json
generated
3
src/i18n/locales/zh-CN/common.json
generated
|
|
@ -196,6 +196,9 @@
|
|||
"cloud_auth_required": "您的组织需要 Roo Code Cloud 身份验证。请登录以继续。",
|
||||
"organization_mismatch": "您必须使用组织的 Roo Code Cloud 账户进行身份验证。",
|
||||
"verification_failed": "无法验证组织身份验证。"
|
||||
},
|
||||
"info": {
|
||||
"organization_requires_auth": "您的组织需要身份验证。"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
|
|
|
|||
6
src/i18n/locales/zh-CN/marketplace.json
generated
6
src/i18n/locales/zh-CN/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "未找到标签。",
|
||||
"selected": "显示包含任一选中标签的项目"
|
||||
},
|
||||
"installed": {
|
||||
"label": "按状态筛选",
|
||||
"all": "所有项目",
|
||||
"installed": "已安装",
|
||||
"notInstalled": "未安装"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "完成",
|
||||
|
|
|
|||
3
src/i18n/locales/zh-TW/common.json
generated
3
src/i18n/locales/zh-TW/common.json
generated
|
|
@ -191,6 +191,9 @@
|
|||
"cloud_auth_required": "您的組織需要 Roo Code Cloud 身份驗證。請登入以繼續。",
|
||||
"organization_mismatch": "您必須使用組織的 Roo Code Cloud 帳戶進行身份驗證。",
|
||||
"verification_failed": "無法驗證組織身份驗證。"
|
||||
},
|
||||
"info": {
|
||||
"organization_requires_auth": "您的組織需要身份驗證。"
|
||||
}
|
||||
},
|
||||
"prompts": {
|
||||
|
|
|
|||
6
src/i18n/locales/zh-TW/marketplace.json
generated
6
src/i18n/locales/zh-TW/marketplace.json
generated
|
|
@ -38,6 +38,12 @@
|
|||
"noResults": "找不到標籤。",
|
||||
"selected": "顯示包含任一選取標籤的項目"
|
||||
},
|
||||
"installed": {
|
||||
"label": "按狀態篩選",
|
||||
"all": "所有項目",
|
||||
"installed": "已安裝",
|
||||
"notInstalled": "未安裝"
|
||||
},
|
||||
"title": "Marketplace"
|
||||
},
|
||||
"done": "完成",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue