Merge branch 'main' into will/mode-plumbing - Resolved conflicts by combining mode plumbing with approve/deny functionality

This commit is contained in:
Will Li 2025-08-20 22:56:33 -07:00
commit de40233890
133 changed files with 3590 additions and 635 deletions

View file

@ -3,3 +3,4 @@ POSTHOG_API_KEY=key-goes-here
# Roo Code Cloud / Local Development
CLERK_BASE_URL=https://epic-chamois-85.clerk.accounts.dev
ROO_CODE_API_URL=http://localhost:3000
ROO_CODE_PROVIDER_URL=http://localhost:8080/proxy/v1

View file

@ -25,6 +25,7 @@ body:
- AWS Bedrock
- Chutes AI
- DeepSeek
- Featherless AI
- Fireworks AI
- Glama
- Google Gemini

View file

@ -1,5 +1,41 @@
# Roo Code Changelog
## [3.25.20] - 2025-08-19
- Add announcement for Sonic model
## [3.25.19] - 2025-08-19
- Fix issue where new users couldn't select the Roo Code Cloud provider (thanks @daniel-lxs!)
## [3.25.18] - 2025-08-19
- Add new stealth Sonic model through the Roo Code Cloud provider
- Fix: respect enableReasoningEffort setting when determining reasoning usage (#7048 by @ikbencasdoei, PR by @app/roomote)
- Fix: prevent duplicate LM Studio models with case-insensitive deduplication (#6954 by @fbuechler, PR by @daniel-lxs)
- Feat: simplify ask_followup_question prompt documentation (thanks @daniel-lxs!)
- Feat: simple read_file tool for single-file-only models (thanks @daniel-lxs!)
- Fix: Add missing zaiApiKey and doubaoApiKey to SECRET_STATE_KEYS (#7082 by @app/roomote)
- Feat: Add new models and update configurations for vscode-lm (thanks @NaccOll!)
## [3.25.17] - 2025-08-17
- Fix: Resolve terminal reuse logic issues
## [3.25.16] - 2025-08-16
- Add support for OpenAI gpt-5-chat-latest model (#7057 by @PeterDaveHello, PR by @app/roomote)
- Fix: Use native Ollama API instead of OpenAI compatibility layer (#7070 by @LivioGama, PR by @daniel-lxs)
- Fix: Prevent XML entity decoding in diff tools (#7107 by @indiesewell, PR by @app/roomote)
- Fix: Add type check before calling .match() on diffItem.content (#6905 by @pwilkin, PR by @app/roomote)
- Refactor task execution system: improve call stack management (thanks @catrielmuller!)
- Fix: Enable save button for provider dropdown and checkbox changes (thanks @daniel-lxs!)
- Add an API for resuming tasks by ID (thanks @mrubens!)
- Emit event when a task ask requires interaction (thanks @cte!)
- Make enhance with task history default to true (thanks @liwilliam2021!)
- Fix: Use cline.cwd as primary source for workspace path in codebaseSearchTool (thanks @NaccOll!)
- Hotfix multiple folder workspace checkpoint (thanks @NaccOll!)
## [3.25.15] - 2025-08-14
- Fix: Remove 500-message limit to prevent scrollbar jumping in long conversations (#7052, #7063 by @daniel-lxs, PR by @app/roomote)

90
packages/ipc/README.md Normal file
View file

@ -0,0 +1,90 @@
# IPC (Inter-Process Communication)
This package provides IPC functionality for Roo Code, allowing external applications to communicate with the extension through a socket-based interface.
## Available Commands
The IPC interface supports the following task commands:
### StartNewTask
Starts a new task with optional configuration and initial message.
**Parameters:**
- `configuration`: RooCode settings object
- `text`: Initial task message (string)
- `images`: Array of image data URIs (optional)
- `newTab`: Whether to open in a new tab (boolean, optional)
### CancelTask
Cancels a running task.
**Parameters:**
- `data`: Task ID to cancel (string)
### CloseTask
Closes a task and performs cleanup.
**Parameters:**
- `data`: Task ID to close (string)
### ResumeTask
Resumes a task from history.
**Parameters:**
- `data`: Task ID to resume (string)
**Error Handling:**
- If the task ID is not found in history, the command will fail gracefully without crashing the IPC server
- Errors are logged for debugging purposes but do not propagate to the client
## Usage Example
```typescript
import { IpcClient } from "@roo-code/ipc"
const client = new IpcClient("/path/to/socket")
// Resume a task
client.sendCommand({
commandName: "ResumeTask",
data: "task-123",
})
// Start a new task
client.sendCommand({
commandName: "StartNewTask",
data: {
configuration: {
/* RooCode settings */
},
text: "Hello, world!",
images: [],
newTab: false,
},
})
```
## Events
The IPC interface also emits task events that clients can listen to:
- `TaskStarted`: When a task begins
- `TaskCompleted`: When a task finishes
- `TaskAborted`: When a task is cancelled
- `Message`: When a task sends a message
## Socket Path
The socket path is typically located in the system's temporary directory and follows the pattern:
- Unix/Linux/macOS: `/tmp/roo-code-{id}.sock`
- Windows: `\\.\pipe\roo-code-{id}`

View file

@ -1,6 +1,6 @@
{
"name": "@roo-code/types",
"version": "1.49.0",
"version": "1.59.0",
"description": "TypeScript type definitions for Roo Code.",
"publishConfig": {
"access": "public",

View file

@ -0,0 +1,75 @@
import { describe, it, expect } from "vitest"
import { TaskCommandName, taskCommandSchema } from "../ipc.js"
describe("IPC Types", () => {
describe("TaskCommandName", () => {
it("should include ResumeTask command", () => {
expect(TaskCommandName.ResumeTask).toBe("ResumeTask")
})
it("should have all expected task commands", () => {
const expectedCommands = ["StartNewTask", "CancelTask", "CloseTask", "ResumeTask"]
const actualCommands = Object.values(TaskCommandName)
expectedCommands.forEach((command) => {
expect(actualCommands).toContain(command)
})
})
describe("Error Handling", () => {
it("should handle ResumeTask command gracefully when task not found", () => {
// This test verifies the schema validation - the actual error handling
// for invalid task IDs is tested at the API level, not the schema level
const resumeTaskCommand = {
commandName: TaskCommandName.ResumeTask,
data: "non-existent-task-id",
}
const result = taskCommandSchema.safeParse(resumeTaskCommand)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.commandName).toBe("ResumeTask")
expect(result.data.data).toBe("non-existent-task-id")
}
})
})
})
describe("taskCommandSchema", () => {
it("should validate ResumeTask command with taskId", () => {
const resumeTaskCommand = {
commandName: TaskCommandName.ResumeTask,
data: "task-123",
}
const result = taskCommandSchema.safeParse(resumeTaskCommand)
expect(result.success).toBe(true)
if (result.success) {
expect(result.data.commandName).toBe("ResumeTask")
expect(result.data.data).toBe("task-123")
}
})
it("should reject ResumeTask command with invalid data", () => {
const invalidCommand = {
commandName: TaskCommandName.ResumeTask,
data: 123, // Should be string
}
const result = taskCommandSchema.safeParse(invalidCommand)
expect(result.success).toBe(false)
})
it("should reject ResumeTask command without data", () => {
const invalidCommand = {
commandName: TaskCommandName.ResumeTask,
// Missing data field
}
const result = taskCommandSchema.safeParse(invalidCommand)
expect(result.success).toBe(false)
})
})
})

View file

@ -18,6 +18,8 @@ export enum RooCodeEventName {
TaskFocused = "taskFocused",
TaskUnfocused = "taskUnfocused",
TaskActive = "taskActive",
TaskInteractive = "taskInteractive",
TaskResumable = "taskResumable",
TaskIdle = "taskIdle",
// Subtask Lifecycle
@ -59,6 +61,8 @@ export const rooCodeEventsSchema = z.object({
[RooCodeEventName.TaskFocused]: z.tuple([z.string()]),
[RooCodeEventName.TaskUnfocused]: z.tuple([z.string()]),
[RooCodeEventName.TaskActive]: z.tuple([z.string()]),
[RooCodeEventName.TaskInteractive]: z.tuple([z.string()]),
[RooCodeEventName.TaskResumable]: z.tuple([z.string()]),
[RooCodeEventName.TaskIdle]: z.tuple([z.string()]),
[RooCodeEventName.TaskPaused]: z.tuple([z.string()]),
@ -124,6 +128,16 @@ export const taskEventSchema = z.discriminatedUnion("eventName", [
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskActive],
taskId: z.number().optional(),
}),
z.object({
eventName: z.literal(RooCodeEventName.TaskInteractive),
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskInteractive],
taskId: z.number().optional(),
}),
z.object({
eventName: z.literal(RooCodeEventName.TaskResumable),
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskResumable],
taskId: z.number().optional(),
}),
z.object({
eventName: z.literal(RooCodeEventName.TaskIdle),
payload: rooCodeEventsSchema.shape[RooCodeEventName.TaskIdle],

View file

@ -178,6 +178,7 @@ export const SECRET_STATE_KEYS = [
"openAiNativeApiKey",
"cerebrasApiKey",
"deepSeekApiKey",
"doubaoApiKey",
"moonshotApiKey",
"mistralApiKey",
"unboundApiKey",
@ -193,7 +194,9 @@ export const SECRET_STATE_KEYS = [
"codebaseIndexMistralApiKey",
"huggingFaceApiKey",
"sambaNovaApiKey",
"zaiApiKey",
"fireworksApiKey",
"featherlessApiKey",
"ioIntelligenceApiKey",
] as const satisfies readonly (keyof ProviderSettings)[]
export type SecretState = Pick<ProviderSettings, (typeof SECRET_STATE_KEYS)[number]>

View file

@ -12,6 +12,7 @@ export * from "./message.js"
export * from "./mode.js"
export * from "./model.js"
export * from "./provider-settings.js"
export * from "./single-file-read-models.js"
export * from "./task.js"
export * from "./todo.js"
export * from "./telemetry.js"

View file

@ -44,6 +44,7 @@ export enum TaskCommandName {
StartNewTask = "StartNewTask",
CancelTask = "CancelTask",
CloseTask = "CloseTask",
ResumeTask = "ResumeTask",
}
/**
@ -68,6 +69,10 @@ export const taskCommandSchema = z.discriminatedUnion("commandName", [
commandName: z.literal(TaskCommandName.CloseTask),
data: z.string(),
}),
z.object({
commandName: z.literal(TaskCommandName.ResumeTask),
data: z.string(),
}),
])
export type TaskCommand = z.infer<typeof taskCommandSchema>

View file

@ -44,24 +44,61 @@ export const clineAskSchema = z.enum(clineAsks)
export type ClineAsk = z.infer<typeof clineAskSchema>
// Needs classification:
// - `followup`
// - `command_output
/**
* BlockingAsk
* IdleAsk
*
* Asks that put the task into an "idle" state.
*/
export const blockingAsks: ClineAsk[] = [
"api_req_failed",
"mistake_limit_reached",
export const idleAsks = [
"completion_result",
"resume_task",
"api_req_failed",
"resume_completed_task",
"command_output",
"mistake_limit_reached",
"auto_approval_max_req_reached",
] as const
] as const satisfies readonly ClineAsk[]
export type BlockingAsk = (typeof blockingAsks)[number]
export type IdleAsk = (typeof idleAsks)[number]
export function isBlockingAsk(ask: ClineAsk): ask is BlockingAsk {
return blockingAsks.includes(ask)
export function isIdleAsk(ask: ClineAsk): ask is IdleAsk {
return (idleAsks as readonly ClineAsk[]).includes(ask)
}
/**
* ResumableAsk
*
* Asks that put the task into an "resumable" state.
*/
export const resumableAsks = ["resume_task"] as const satisfies readonly ClineAsk[]
export type ResumableAsk = (typeof resumableAsks)[number]
export function isResumableAsk(ask: ClineAsk): ask is ResumableAsk {
return (resumableAsks as readonly ClineAsk[]).includes(ask)
}
/**
* InteractiveAsk
*
* Asks that put the task into an "user interaction required" state.
*/
export const interactiveAsks = [
"command",
"tool",
"browser_action_launch",
"use_mcp_server",
] as const satisfies readonly ClineAsk[]
export type InteractiveAsk = (typeof interactiveAsks)[number]
export function isInteractiveAsk(ask: ClineAsk): ask is InteractiveAsk {
return (interactiveAsks as readonly ClineAsk[]).includes(ask)
}
/**

View file

@ -10,6 +10,14 @@ export const reasoningEffortsSchema = z.enum(reasoningEfforts)
export type ReasoningEffort = z.infer<typeof reasoningEffortsSchema>
/**
* ReasoningEffortWithMinimal
*/
export const reasoningEffortWithMinimalSchema = z.union([reasoningEffortsSchema, z.literal("minimal")])
export type ReasoningEffortWithMinimal = z.infer<typeof reasoningEffortWithMinimalSchema>
/**
* Verbosity
*/

View file

@ -1,15 +1,30 @@
import { z } from "zod"
import { reasoningEffortsSchema, verbosityLevelsSchema, modelInfoSchema } from "./model.js"
import { modelInfoSchema, reasoningEffortWithMinimalSchema, verbosityLevelsSchema } from "./model.js"
import { codebaseIndexProviderSchema } from "./codebase-index.js"
// Bedrock Claude Sonnet 4 model ID that supports 1M context
export const BEDROCK_CLAUDE_SONNET_4_MODEL_ID = "anthropic.claude-sonnet-4-20250514-v1:0"
// Extended schema that includes "minimal" for GPT-5 models
export const extendedReasoningEffortsSchema = z.union([reasoningEffortsSchema, z.literal("minimal")])
export type ReasoningEffortWithMinimal = z.infer<typeof extendedReasoningEffortsSchema>
import {
anthropicModels,
bedrockModels,
cerebrasModels,
chutesModels,
claudeCodeModels,
deepSeekModels,
doubaoModels,
featherlessModels,
fireworksModels,
geminiModels,
groqModels,
ioIntelligenceModels,
mistralModels,
moonshotModels,
openAiNativeModels,
rooModels,
sambaNovaModels,
vertexModels,
vscodeLlmModels,
xaiModels,
internationalZAiModels,
} from "./providers/index.js"
/**
* ProviderName
@ -46,7 +61,9 @@ export const providerNames = [
"sambanova",
"zai",
"fireworks",
"featherless",
"io-intelligence",
"roo",
] as const
export const providerNamesSchema = z.enum(providerNames)
@ -85,7 +102,7 @@ const baseProviderSettingsSchema = z.object({
// Model reasoning.
enableReasoningEffort: z.boolean().optional(),
reasoningEffort: extendedReasoningEffortsSchema.optional(),
reasoningEffort: reasoningEffortWithMinimalSchema.optional(),
modelMaxTokens: z.number().optional(),
modelMaxThinkingTokens: z.number().optional(),
@ -283,11 +300,19 @@ const fireworksSchema = apiModelIdProviderModelSchema.extend({
fireworksApiKey: z.string().optional(),
})
const featherlessSchema = apiModelIdProviderModelSchema.extend({
featherlessApiKey: z.string().optional(),
})
const ioIntelligenceSchema = apiModelIdProviderModelSchema.extend({
ioIntelligenceModelId: z.string().optional(),
ioIntelligenceApiKey: z.string().optional(),
})
const rooSchema = apiModelIdProviderModelSchema.extend({
// No additional fields needed - uses cloud authentication
})
const defaultSchema = z.object({
apiProvider: z.undefined(),
})
@ -323,7 +348,9 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
sambaNovaSchema.merge(z.object({ apiProvider: z.literal("sambanova") })),
zaiSchema.merge(z.object({ apiProvider: z.literal("zai") })),
fireworksSchema.merge(z.object({ apiProvider: z.literal("fireworks") })),
featherlessSchema.merge(z.object({ apiProvider: z.literal("featherless") })),
ioIntelligenceSchema.merge(z.object({ apiProvider: z.literal("io-intelligence") })),
rooSchema.merge(z.object({ apiProvider: z.literal("roo") })),
defaultSchema,
])
@ -359,7 +386,9 @@ export const providerSettingsSchema = z.object({
...sambaNovaSchema.shape,
...zaiSchema.shape,
...fireworksSchema.shape,
...featherlessSchema.shape,
...ioIntelligenceSchema.shape,
...rooSchema.shape,
...codebaseIndexProviderSchema.shape,
})
@ -393,21 +422,126 @@ export const getModelId = (settings: ProviderSettings): string | undefined => {
return modelIdKey ? (settings[modelIdKey] as string) : undefined
}
// Providers that use Anthropic-style API protocol
// Providers that use Anthropic-style API protocol.
export const ANTHROPIC_STYLE_PROVIDERS: ProviderName[] = ["anthropic", "claude-code", "bedrock"]
// Helper function to determine API protocol for a provider and model
export const getApiProtocol = (provider: ProviderName | undefined, modelId?: string): "anthropic" | "openai" => {
// First check if the provider is an Anthropic-style provider
if (provider && ANTHROPIC_STYLE_PROVIDERS.includes(provider)) {
return "anthropic"
}
// For vertex provider, check if the model ID contains "claude" (case-insensitive)
if (provider && provider === "vertex" && modelId && modelId.toLowerCase().includes("claude")) {
return "anthropic"
}
// Default to OpenAI protocol
return "openai"
}
export const MODELS_BY_PROVIDER: Record<
Exclude<ProviderName, "fake-ai" | "human-relay" | "gemini-cli" | "lmstudio" | "openai" | "ollama">,
{ id: ProviderName; label: string; models: string[] }
> = {
anthropic: {
id: "anthropic",
label: "Anthropic",
models: Object.keys(anthropicModels),
},
bedrock: {
id: "bedrock",
label: "Amazon Bedrock",
models: Object.keys(bedrockModels),
},
cerebras: {
id: "cerebras",
label: "Cerebras",
models: Object.keys(cerebrasModels),
},
chutes: {
id: "chutes",
label: "Chutes AI",
models: Object.keys(chutesModels),
},
"claude-code": { id: "claude-code", label: "Claude Code", models: Object.keys(claudeCodeModels) },
deepseek: {
id: "deepseek",
label: "DeepSeek",
models: Object.keys(deepSeekModels),
},
doubao: { id: "doubao", label: "Doubao", models: Object.keys(doubaoModels) },
featherless: {
id: "featherless",
label: "Featherless",
models: Object.keys(featherlessModels),
},
fireworks: {
id: "fireworks",
label: "Fireworks",
models: Object.keys(fireworksModels),
},
gemini: {
id: "gemini",
label: "Google Gemini",
models: Object.keys(geminiModels),
},
groq: { id: "groq", label: "Groq", models: Object.keys(groqModels) },
"io-intelligence": {
id: "io-intelligence",
label: "IO Intelligence",
models: Object.keys(ioIntelligenceModels),
},
mistral: {
id: "mistral",
label: "Mistral",
models: Object.keys(mistralModels),
},
moonshot: {
id: "moonshot",
label: "Moonshot",
models: Object.keys(moonshotModels),
},
"openai-native": {
id: "openai-native",
label: "OpenAI",
models: Object.keys(openAiNativeModels),
},
roo: { id: "roo", label: "Roo", models: Object.keys(rooModels) },
sambanova: {
id: "sambanova",
label: "SambaNova",
models: Object.keys(sambaNovaModels),
},
vertex: {
id: "vertex",
label: "GCP Vertex AI",
models: Object.keys(vertexModels),
},
"vscode-lm": {
id: "vscode-lm",
label: "VS Code LM API",
models: Object.keys(vscodeLlmModels),
},
xai: { id: "xai", label: "xAI (Grok)", models: Object.keys(xaiModels) },
zai: { id: "zai", label: "Zai", models: Object.keys(internationalZAiModels) },
// Dynamic providers; models pulled from the respective APIs.
glama: { id: "glama", label: "Glama", models: [] },
huggingface: { id: "huggingface", label: "Hugging Face", models: [] },
litellm: { id: "litellm", label: "LiteLLM", models: [] },
openrouter: { id: "openrouter", label: "OpenRouter", models: [] },
requesty: { id: "requesty", label: "Requesty", models: [] },
unbound: { id: "unbound", label: "Unbound", models: [] },
}
export const dynamicProviders = [
"glama",
"huggingface",
"litellm",
"openrouter",
"requesty",
"unbound",
] as const satisfies readonly ProviderName[]
export type DynamicProvider = (typeof dynamicProviders)[number]
export const isDynamicProvider = (key: string): key is DynamicProvider =>
dynamicProviders.includes(key as DynamicProvider)

View file

@ -441,3 +441,5 @@ export const BEDROCK_REGIONS = [
{ value: "us-gov-east-1", label: "us-gov-east-1" },
{ value: "us-gov-west-1", label: "us-gov-west-1" },
].sort((a, b) => a.value.localeCompare(b.value))
export const BEDROCK_CLAUDE_SONNET_4_MODEL_ID = "anthropic.claude-sonnet-4-20250514-v1:0"

View file

@ -0,0 +1,58 @@
import type { ModelInfo } from "../model.js"
export type FeatherlessModelId =
| "deepseek-ai/DeepSeek-V3-0324"
| "deepseek-ai/DeepSeek-R1-0528"
| "moonshotai/Kimi-K2-Instruct"
| "openai/gpt-oss-120b"
| "Qwen/Qwen3-Coder-480B-A35B-Instruct"
export const featherlessModels = {
"deepseek-ai/DeepSeek-V3-0324": {
maxTokens: 4096,
contextWindow: 32678,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek V3 0324 model.",
},
"deepseek-ai/DeepSeek-R1-0528": {
maxTokens: 4096,
contextWindow: 32678,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "DeepSeek R1 0528 model.",
},
"moonshotai/Kimi-K2-Instruct": {
maxTokens: 4096,
contextWindow: 32678,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Kimi K2 Instruct model.",
},
"openai/gpt-oss-120b": {
maxTokens: 4096,
contextWindow: 32678,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "GPT-OSS 120B model.",
},
"Qwen/Qwen3-Coder-480B-A35B-Instruct": {
maxTokens: 4096,
contextWindow: 32678,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
description: "Qwen3 Coder 480B A35B Instruct model.",
},
} as const satisfies Record<string, ModelInfo>
export const featherlessDefaultModelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528"

View file

@ -4,6 +4,9 @@ export * from "./cerebras.js"
export * from "./chutes.js"
export * from "./claude-code.js"
export * from "./deepseek.js"
export * from "./doubao.js"
export * from "./featherless.js"
export * from "./fireworks.js"
export * from "./gemini.js"
export * from "./glama.js"
export * from "./groq.js"
@ -17,11 +20,10 @@ export * from "./ollama.js"
export * from "./openai.js"
export * from "./openrouter.js"
export * from "./requesty.js"
export * from "./roo.js"
export * from "./sambanova.js"
export * from "./unbound.js"
export * from "./vertex.js"
export * from "./vscode-llm.js"
export * from "./xai.js"
export * from "./doubao.js"
export * from "./zai.js"
export * from "./fireworks.js"

View file

@ -6,6 +6,18 @@ export type OpenAiNativeModelId = keyof typeof openAiNativeModels
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-5-2025-08-07"
export const openAiNativeModels = {
"gpt-5-chat-latest": {
maxTokens: 128000,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: false,
inputPrice: 1.25,
outputPrice: 10.0,
cacheReadsPrice: 0.13,
description: "GPT-5 Chat Latest: Optimized for conversational AI and non-reasoning tasks",
supportsVerbosity: true,
},
"gpt-5-2025-08-07": {
maxTokens: 128000,
contextWindow: 400000,

View file

@ -0,0 +1,19 @@
import type { ModelInfo } from "../model.js"
// Roo provider with single model
export type RooModelId = "roo/sonic"
export const rooDefaultModelId: RooModelId = "roo/sonic"
export const rooModels = {
"roo/sonic": {
maxTokens: 16_384,
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: true,
inputPrice: 0,
outputPrice: 0,
description:
"A stealth reasoning model that is blazing fast and excels at agentic coding, accessible for free through Roo Code Cloud for a limited time. (Note: prompts and completions are logged by the model creator and used to improve the model.)",
},
} as const satisfies Record<string, ModelInfo>

View file

@ -4,6 +4,7 @@ export type VscodeLlmModelId = keyof typeof vscodeLlmModels
export const vscodeLlmDefaultModelId: VscodeLlmModelId = "claude-3.5-sonnet"
// https://docs.cline.bot/provider-config/vscode-language-model-api
export const vscodeLlmModels = {
"gpt-3.5-turbo": {
contextWindow: 12114,
@ -101,6 +102,18 @@ export const vscodeLlmModels = {
supportsToolCalling: true,
maxInputTokens: 81638,
},
"claude-4-sonnet": {
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
family: "claude-sonnet-4",
version: "claude-sonnet-4",
name: "Claude Sonnet 4",
supportsToolCalling: true,
maxInputTokens: 111836,
},
"gemini-2.0-flash-001": {
contextWindow: 127827,
supportsImages: true,
@ -114,7 +127,7 @@ export const vscodeLlmModels = {
maxInputTokens: 127827,
},
"gemini-2.5-pro": {
contextWindow: 63830,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
@ -123,10 +136,10 @@ export const vscodeLlmModels = {
version: "gemini-2.5-pro-preview-03-25",
name: "Gemini 2.5 Pro (Preview)",
supportsToolCalling: true,
maxInputTokens: 63830,
maxInputTokens: 108637,
},
"o4-mini": {
contextWindow: 111446,
contextWindow: 128000,
supportsImages: false,
supportsPromptCache: false,
inputPrice: 0,
@ -135,10 +148,10 @@ export const vscodeLlmModels = {
version: "o4-mini-2025-04-16",
name: "o4-mini (Preview)",
supportsToolCalling: true,
maxInputTokens: 111446,
maxInputTokens: 111452,
},
"gpt-4.1": {
contextWindow: 111446,
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
@ -147,7 +160,31 @@ export const vscodeLlmModels = {
version: "gpt-4.1-2025-04-14",
name: "GPT-4.1 (Preview)",
supportsToolCalling: true,
maxInputTokens: 111446,
maxInputTokens: 111452,
},
"gpt-5-mini": {
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
family: "gpt-5-mini",
version: "gpt-5-mini",
name: "GPT-5 mini (Preview)",
supportsToolCalling: true,
maxInputTokens: 108637,
},
"gpt-5": {
contextWindow: 128000,
supportsImages: true,
supportsPromptCache: false,
inputPrice: 0,
outputPrice: 0,
family: "gpt-5",
version: "gpt-5",
name: "GPT-5 (Preview)",
supportsToolCalling: true,
maxInputTokens: 108637,
},
} as const satisfies Record<
string,

View file

@ -0,0 +1,32 @@
/**
* Configuration for models that should use simplified single-file read_file tool
* These models will use the simpler <read_file><path>...</path></read_file> format
* instead of the more complex multi-file args format
*/
// List of model IDs (or patterns) that should use single file reads only
export const SINGLE_FILE_READ_MODELS = new Set<string>(["roo/sonic"])
/**
* Check if a model should use single file read format
* @param modelId The model ID to check
* @returns true if the model should use single file reads
*/
export function shouldUseSingleFileRead(modelId: string): boolean {
// Direct match
if (SINGLE_FILE_READ_MODELS.has(modelId)) {
return true
}
// Pattern matching for model families
// Check if model ID starts with any configured pattern
// Using Array.from for compatibility with older TypeScript targets
const patterns = Array.from(SINGLE_FILE_READ_MODELS)
for (const pattern of patterns) {
if (pattern.endsWith("*") && modelId.startsWith(pattern.slice(0, -1))) {
return true
}
}
return false
}

View file

@ -1,7 +1,7 @@
import { z } from "zod"
import { RooCodeEventName } from "./events.js"
import { type ClineMessage, type BlockingAsk, type TokenUsage } from "./message.js"
import { type ClineMessage, type TokenUsage } from "./message.js"
import { type ToolUsage, type ToolName } from "./tool.js"
import { type Experiments } from "./experiment.js"
import type { StaticAppProperties, GitProperties, TelemetryProperties } from "./telemetry.js"
@ -34,6 +34,7 @@ export interface TaskProviderLike {
createTask(text?: string, images?: string[], parentTask?: TaskLike, options?: CreateTaskOptions): Promise<TaskLike>
cancelTask(): Promise<void>
clearTask(): Promise<void>
resumeTask(taskId: string): void
getState(): Promise<TaskProviderState>
postStateToWebview(): Promise<void>
@ -62,6 +63,8 @@ export type TaskProviderEvents = {
[RooCodeEventName.TaskFocused]: [taskId: string]
[RooCodeEventName.TaskUnfocused]: [taskId: string]
[RooCodeEventName.TaskActive]: [taskId: string]
[RooCodeEventName.TaskInteractive]: [taskId: string]
[RooCodeEventName.TaskResumable]: [taskId: string]
[RooCodeEventName.TaskIdle]: [taskId: string]
}
@ -69,8 +72,15 @@ export type TaskProviderEvents = {
* TaskLike
*/
export enum TaskStatus {
Running = "running",
Interactive = "interactive",
Resumable = "resumable",
Idle = "idle",
None = "none",
}
export const taskMetadataSchema = z.object({
taskId: z.string(),
task: z.string().optional(),
images: z.array(z.string()).optional(),
})
@ -79,15 +89,20 @@ export type TaskMetadata = z.infer<typeof taskMetadataSchema>
export interface TaskLike {
readonly taskId: string
readonly rootTask?: TaskLike
readonly blockingAsk?: BlockingAsk
readonly taskStatus: TaskStatus
readonly taskAsk: ClineMessage | undefined
readonly metadata: TaskMetadata
readonly rootTask?: TaskLike
on<K extends keyof TaskEvents>(event: K, listener: (...args: TaskEvents[K]) => void | Promise<void>): this
off<K extends keyof TaskEvents>(event: K, listener: (...args: TaskEvents[K]) => void | Promise<void>): this
setMessageResponse(text: string, images?: string[]): void
approveAsk(options?: { text?: string; images?: string[] }): void
denyAsk(options?: { text?: string; images?: string[] }): void
submitUserMessage(text: string, images?: string[], modeSlug?: string): void
abortTask(): void
}
export type TaskEvents = {
@ -98,6 +113,8 @@ export type TaskEvents = {
[RooCodeEventName.TaskFocused]: []
[RooCodeEventName.TaskUnfocused]: []
[RooCodeEventName.TaskActive]: [taskId: string]
[RooCodeEventName.TaskInteractive]: [taskId: string]
[RooCodeEventName.TaskResumable]: [taskId: string]
[RooCodeEventName.TaskIdle]: [taskId: string]
// Subtask Lifecycle

35
pnpm-lock.yaml generated
View file

@ -584,8 +584,8 @@ importers:
specifier: ^1.14.0
version: 1.14.0(typescript@5.8.3)
'@roo-code/cloud':
specifier: ^0.15.0
version: 0.15.0
specifier: ^0.19.0
version: 0.19.0
'@roo-code/ipc':
specifier: workspace:^
version: link:../packages/ipc
@ -676,6 +676,9 @@ importers:
node-ipc:
specifier: ^12.0.0
version: 12.0.0
ollama:
specifier: ^0.5.17
version: 0.5.17
openai:
specifier: ^5.0.0
version: 5.5.1(ws@8.18.3)(zod@3.25.61)
@ -3103,11 +3106,11 @@ packages:
cpu: [x64]
os: [win32]
'@roo-code/cloud@0.15.0':
resolution: {integrity: sha512-0DivOP5uUS9U6UKSxzoxZ4NxMCYUxA7wG72y3PBP91JhGzHaTqxi9WrvF61bo134dCnCktU9oTKIAD9AvFigzg==}
'@roo-code/cloud@0.19.0':
resolution: {integrity: sha512-alZ3X4+TPqRr0xSs9v/UDo3eTlcHaI8ZW8AbWPDtgqf86P8govnyM2hVUMhGXete3AlbYIPRE/9w3/7MrcIjsA==}
'@roo-code/types@1.49.0':
resolution: {integrity: sha512-h7gbjfIxBN+fgFecQiZs3W+vjdUhZOvtjh4OqcpPGG1w8B1DlXPDV4L/+ARUeNzZxSOK5S5rNPy57jC35EaD/w==}
'@roo-code/types@1.55.0':
resolution: {integrity: sha512-+T5MP8IQcDp7htnGDnk3M4n7S5eYk6jNkw3VBSUBZRhS4EE2GuPDI+CcdmhnDiMb6NMV6yseL+CT4G4QV5ktUw==}
'@sec-ant/readable-stream@0.4.1':
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
@ -7645,6 +7648,9 @@ packages:
resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
engines: {node: '>= 0.4'}
ollama@0.5.17:
resolution: {integrity: sha512-q5LmPtk6GLFouS+3aURIVl+qcAOPC4+Msmx7uBb3pd+fxI55WnGjmLZ0yijI/CYy79x0QPGx3BwC3u5zv9fBvQ==}
on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'}
@ -9655,6 +9661,9 @@ packages:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
whatwg-fetch@3.6.20:
resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==}
whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
engines: {node: '>=18'}
@ -12305,9 +12314,9 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.40.2':
optional: true
'@roo-code/cloud@0.15.0':
'@roo-code/cloud@0.19.0':
dependencies:
'@roo-code/types': 1.49.0
'@roo-code/types': 1.55.0
ioredis: 5.6.1
p-wait-for: 5.0.2
socket.io-client: 4.8.1
@ -12317,7 +12326,7 @@ snapshots:
- supports-color
- utf-8-validate
'@roo-code/types@1.49.0':
'@roo-code/types@1.55.0':
dependencies:
zod: 3.25.76
@ -14176,7 +14185,7 @@ snapshots:
dependencies:
devtools-protocol: 0.0.1452169
mitt: 3.0.1
zod: 3.25.61
zod: 3.25.76
ci-info@2.0.0: {}
@ -17683,6 +17692,10 @@ snapshots:
define-properties: 1.2.1
es-object-atoms: 1.1.1
ollama@0.5.17:
dependencies:
whatwg-fetch: 3.6.20
on-finished@2.4.1:
dependencies:
ee-first: 1.1.1
@ -20155,6 +20168,8 @@ snapshots:
dependencies:
iconv-lite: 0.6.3
whatwg-fetch@3.6.20: {}
whatwg-mimetype@4.0.0: {}
whatwg-url@14.2.0:

View file

@ -13,7 +13,6 @@ import {
VertexHandler,
AnthropicVertexHandler,
OpenAiHandler,
OllamaHandler,
LmStudioHandler,
GeminiHandler,
OpenAiNativeHandler,
@ -36,7 +35,10 @@ import {
DoubaoHandler,
ZAiHandler,
FireworksHandler,
RooHandler,
FeatherlessHandler,
} from "./providers"
import { NativeOllamaHandler } from "./providers/native-ollama"
export interface SingleCompletionHandler {
completePrompt(prompt: string): Promise<string>
@ -95,7 +97,7 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
case "openai":
return new OpenAiHandler(options)
case "ollama":
return new OllamaHandler(options)
return new NativeOllamaHandler(options)
case "lmstudio":
return new LmStudioHandler(options)
case "gemini":
@ -140,6 +142,10 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler {
return new FireworksHandler(options)
case "io-intelligence":
return new IOIntelligenceHandler(options)
case "roo":
return new RooHandler(options)
case "featherless":
return new FeatherlessHandler(options)
default:
apiProvider satisfies "gemini-cli" | undefined
return new AnthropicHandler(options)

View file

@ -0,0 +1,286 @@
// npx vitest run api/providers/__tests__/featherless.spec.ts
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import {
type FeatherlessModelId,
featherlessDefaultModelId,
featherlessModels,
DEEP_SEEK_DEFAULT_TEMPERATURE,
} from "@roo-code/types"
import { FeatherlessHandler } from "../featherless"
// Create mock functions
const mockCreate = vi.fn()
// Mock OpenAI module
vi.mock("openai", () => ({
default: vi.fn(() => ({
chat: {
completions: {
create: mockCreate,
},
},
})),
}))
describe("FeatherlessHandler", () => {
let handler: FeatherlessHandler
beforeEach(() => {
vi.clearAllMocks()
// Set up default mock implementation
mockCreate.mockImplementation(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: { content: "Test response" },
index: 0,
},
],
usage: null,
}
yield {
choices: [
{
delta: {},
index: 0,
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
}
},
}))
handler = new FeatherlessHandler({ featherlessApiKey: "test-key" })
})
afterEach(() => {
vi.restoreAllMocks()
})
it("should use the correct Featherless base URL", () => {
new FeatherlessHandler({ featherlessApiKey: "test-featherless-api-key" })
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.featherless.ai/v1" }))
})
it("should use the provided API key", () => {
const featherlessApiKey = "test-featherless-api-key"
new FeatherlessHandler({ featherlessApiKey })
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: featherlessApiKey }))
})
it("should handle DeepSeek R1 reasoning format", async () => {
// Override the mock for this specific test
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: { content: "<think>Thinking..." },
index: 0,
},
],
usage: null,
}
yield {
choices: [
{
delta: { content: "</think>Hello" },
index: 0,
},
],
usage: null,
}
yield {
choices: [
{
delta: {},
index: 0,
},
],
usage: { prompt_tokens: 10, completion_tokens: 5 },
}
},
}))
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }]
vi.spyOn(handler, "getModel").mockReturnValue({
id: "deepseek-ai/DeepSeek-R1-0528",
info: { maxTokens: 1024, temperature: 0.7 },
} as any)
const stream = handler.createMessage(systemPrompt, messages)
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
expect(chunks).toEqual([
{ type: "reasoning", text: "Thinking..." },
{ type: "text", text: "Hello" },
{ type: "usage", inputTokens: 10, outputTokens: 5 },
])
})
it("should fall back to base provider for non-DeepSeek models", async () => {
// Use default mock implementation which returns text content
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hi" }]
vi.spyOn(handler, "getModel").mockReturnValue({
id: "some-other-model",
info: { maxTokens: 1024, temperature: 0.7 },
} as any)
const stream = handler.createMessage(systemPrompt, messages)
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}
expect(chunks).toEqual([
{ type: "text", text: "Test response" },
{ type: "usage", inputTokens: 10, outputTokens: 5 },
])
})
it("should return default model when no model is specified", () => {
const model = handler.getModel()
expect(model.id).toBe(featherlessDefaultModelId)
expect(model.info).toEqual(expect.objectContaining(featherlessModels[featherlessDefaultModelId]))
})
it("should return specified model when valid model is provided", () => {
const testModelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528"
const handlerWithModel = new FeatherlessHandler({
apiModelId: testModelId,
featherlessApiKey: "test-featherless-api-key",
})
const model = handlerWithModel.getModel()
expect(model.id).toBe(testModelId)
expect(model.info).toEqual(expect.objectContaining(featherlessModels[testModelId]))
})
it("completePrompt method should return text from Featherless API", async () => {
const expectedResponse = "This is a test response from Featherless"
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] })
const result = await handler.completePrompt("test prompt")
expect(result).toBe(expectedResponse)
})
it("should handle errors in completePrompt", async () => {
const errorMessage = "Featherless API error"
mockCreate.mockRejectedValueOnce(new Error(errorMessage))
await expect(handler.completePrompt("test prompt")).rejects.toThrow(
`Featherless completion error: ${errorMessage}`,
)
})
it("createMessage should yield text content from stream", async () => {
const testContent = "This is test content from Featherless stream"
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: vi
.fn()
.mockResolvedValueOnce({
done: false,
value: { choices: [{ delta: { content: testContent } }] },
})
.mockResolvedValueOnce({ done: true }),
}),
}
})
const stream = handler.createMessage("system prompt", [])
const firstChunk = await stream.next()
expect(firstChunk.done).toBe(false)
expect(firstChunk.value).toEqual({ type: "text", text: testContent })
})
it("createMessage should yield usage data from stream", async () => {
mockCreate.mockImplementationOnce(() => {
return {
[Symbol.asyncIterator]: () => ({
next: vi
.fn()
.mockResolvedValueOnce({
done: false,
value: { choices: [{ delta: {} }], usage: { prompt_tokens: 10, completion_tokens: 20 } },
})
.mockResolvedValueOnce({ done: true }),
}),
}
})
const stream = handler.createMessage("system prompt", [])
const firstChunk = await stream.next()
expect(firstChunk.done).toBe(false)
expect(firstChunk.value).toEqual({ type: "usage", inputTokens: 10, outputTokens: 20 })
})
it("createMessage should pass correct parameters to Featherless client for DeepSeek R1", async () => {
const modelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528"
// Clear previous mocks and set up new implementation
mockCreate.mockClear()
mockCreate.mockImplementationOnce(async () => ({
[Symbol.asyncIterator]: async function* () {
// Empty stream for this test
},
}))
const handlerWithModel = new FeatherlessHandler({
apiModelId: modelId,
featherlessApiKey: "test-featherless-api-key",
})
const systemPrompt = "Test system prompt for Featherless"
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message for Featherless" }]
const messageGenerator = handlerWithModel.createMessage(systemPrompt, messages)
await messageGenerator.next()
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: modelId,
messages: [
{
role: "user",
content: `${systemPrompt}\n${messages[0].content}`,
},
],
}),
)
})
it("should apply DeepSeek default temperature for R1 models", () => {
const testModelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528"
const handlerWithModel = new FeatherlessHandler({
apiModelId: testModelId,
featherlessApiKey: "test-featherless-api-key",
})
const model = handlerWithModel.getModel()
expect(model.info.temperature).toBe(DEEP_SEEK_DEFAULT_TEMPERATURE)
})
it("should use default temperature for non-DeepSeek models", () => {
const testModelId: FeatherlessModelId = "moonshotai/Kimi-K2-Instruct"
const handlerWithModel = new FeatherlessHandler({
apiModelId: testModelId,
featherlessApiKey: "test-featherless-api-key",
})
const model = handlerWithModel.getModel()
expect(model.info.temperature).toBe(0.5)
})
})

View file

@ -0,0 +1,162 @@
// npx vitest run api/providers/__tests__/native-ollama.spec.ts
import { NativeOllamaHandler } from "../native-ollama"
import { ApiHandlerOptions } from "../../../shared/api"
// Mock the ollama package
const mockChat = vitest.fn()
vitest.mock("ollama", () => {
return {
Ollama: vitest.fn().mockImplementation(() => ({
chat: mockChat,
})),
Message: vitest.fn(),
}
})
// Mock the getOllamaModels function
vitest.mock("../fetchers/ollama", () => ({
getOllamaModels: vitest.fn().mockResolvedValue({
llama2: {
contextWindow: 4096,
maxTokens: 4096,
supportsImages: false,
supportsPromptCache: false,
},
}),
}))
describe("NativeOllamaHandler", () => {
let handler: NativeOllamaHandler
beforeEach(() => {
vitest.clearAllMocks()
const options: ApiHandlerOptions = {
apiModelId: "llama2",
ollamaModelId: "llama2",
ollamaBaseUrl: "http://localhost:11434",
}
handler = new NativeOllamaHandler(options)
})
describe("createMessage", () => {
it("should stream messages from Ollama", async () => {
// Mock the chat response as an async generator
mockChat.mockImplementation(async function* () {
yield {
message: { content: "Hello" },
eval_count: undefined,
prompt_eval_count: undefined,
}
yield {
message: { content: " world" },
eval_count: 2,
prompt_eval_count: 10,
}
})
const systemPrompt = "You are a helpful assistant"
const messages = [{ role: "user" as const, content: "Hi there" }]
const stream = handler.createMessage(systemPrompt, messages)
const results = []
for await (const chunk of stream) {
results.push(chunk)
}
expect(results).toHaveLength(3)
expect(results[0]).toEqual({ type: "text", text: "Hello" })
expect(results[1]).toEqual({ type: "text", text: " world" })
expect(results[2]).toEqual({ type: "usage", inputTokens: 10, outputTokens: 2 })
})
it("should handle DeepSeek R1 models with reasoning detection", async () => {
const options: ApiHandlerOptions = {
apiModelId: "deepseek-r1",
ollamaModelId: "deepseek-r1",
ollamaBaseUrl: "http://localhost:11434",
}
handler = new NativeOllamaHandler(options)
// Mock response with thinking tags
mockChat.mockImplementation(async function* () {
yield { message: { content: "<think>Let me think" } }
yield { message: { content: " about this</think>" } }
yield { message: { content: "The answer is 42" } }
})
const stream = handler.createMessage("System", [{ role: "user" as const, content: "Question?" }])
const results = []
for await (const chunk of stream) {
results.push(chunk)
}
// Should detect reasoning vs regular text
expect(results.some((r) => r.type === "reasoning")).toBe(true)
expect(results.some((r) => r.type === "text")).toBe(true)
})
})
describe("completePrompt", () => {
it("should complete a prompt without streaming", async () => {
mockChat.mockResolvedValue({
message: { content: "This is the response" },
})
const result = await handler.completePrompt("Tell me a joke")
expect(mockChat).toHaveBeenCalledWith({
model: "llama2",
messages: [{ role: "user", content: "Tell me a joke" }],
stream: false,
options: {
temperature: 0,
},
})
expect(result).toBe("This is the response")
})
})
describe("error handling", () => {
it("should handle connection refused errors", async () => {
const error = new Error("ECONNREFUSED") as any
error.code = "ECONNREFUSED"
mockChat.mockRejectedValue(error)
const stream = handler.createMessage("System", [{ role: "user" as const, content: "Test" }])
await expect(async () => {
for await (const _ of stream) {
// consume stream
}
}).rejects.toThrow("Ollama service is not running")
})
it("should handle model not found errors", async () => {
const error = new Error("Not found") as any
error.status = 404
mockChat.mockRejectedValue(error)
const stream = handler.createMessage("System", [{ role: "user" as const, content: "Test" }])
await expect(async () => {
for await (const _ of stream) {
// consume stream
}
}).rejects.toThrow("Model llama2 not found in Ollama")
})
})
describe("getModel", () => {
it("should return the configured model", () => {
const model = handler.getModel()
expect(model.id).toBe("llama2")
expect(model.info).toBeDefined()
})
})
})

View file

@ -0,0 +1,436 @@
// npx vitest run api/providers/__tests__/roo.spec.ts
import { Anthropic } from "@anthropic-ai/sdk"
import { rooDefaultModelId, rooModels } from "@roo-code/types"
import { ApiHandlerOptions } from "../../../shared/api"
// Mock OpenAI client
const mockCreate = vitest.fn()
vitest.mock("openai", () => {
return {
__esModule: true,
default: vitest.fn().mockImplementation(() => ({
chat: {
completions: {
create: mockCreate.mockImplementation(async (options) => {
if (!options.stream) {
return {
id: "test-completion",
choices: [
{
message: { role: "assistant", content: "Test response" },
finish_reason: "stop",
index: 0,
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
}
}
return {
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: { content: "Test response" },
index: 0,
},
],
usage: null,
}
yield {
choices: [
{
delta: {},
index: 0,
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
},
}
},
}
}),
},
},
})),
}
})
// Mock CloudService - Define functions outside to avoid initialization issues
const mockGetSessionToken = vitest.fn()
const mockHasInstance = vitest.fn()
// Create mock functions that we can control
const mockGetSessionTokenFn = vitest.fn()
const mockHasInstanceFn = vitest.fn()
vitest.mock("@roo-code/cloud", () => ({
CloudService: {
hasInstance: () => mockHasInstanceFn(),
get instance() {
return {
authService: {
getSessionToken: () => mockGetSessionTokenFn(),
},
}
},
},
}))
// Mock i18n
vitest.mock("../../../i18n", () => ({
t: vitest.fn((key: string) => {
if (key === "common:errors.roo.authenticationRequired") {
return "Authentication required for Roo Code Cloud"
}
return key
}),
}))
// Import after mocks are set up
import { RooHandler } from "../roo"
import { CloudService } from "@roo-code/cloud"
import { t } from "../../../i18n"
describe("RooHandler", () => {
let handler: RooHandler
let mockOptions: ApiHandlerOptions
const systemPrompt = "You are a helpful assistant."
const messages: Anthropic.Messages.MessageParam[] = [
{
role: "user",
content: "Hello!",
},
]
beforeEach(() => {
mockOptions = {
apiModelId: "roo/sonic",
}
// Set up CloudService mocks for successful authentication
mockHasInstanceFn.mockReturnValue(true)
mockGetSessionTokenFn.mockReturnValue("test-session-token")
mockCreate.mockClear()
vitest.clearAllMocks()
})
describe("constructor", () => {
it("should initialize with valid session token", () => {
handler = new RooHandler(mockOptions)
expect(handler).toBeInstanceOf(RooHandler)
expect(handler.getModel().id).toBe(mockOptions.apiModelId)
})
it("should 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")
})
it("should 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")
})
it("should initialize with default model if no model specified", () => {
handler = new RooHandler({})
expect(handler).toBeInstanceOf(RooHandler)
expect(handler.getModel().id).toBe(rooDefaultModelId)
})
it("should pass correct configuration to base class", () => {
handler = new RooHandler(mockOptions)
expect(handler).toBeInstanceOf(RooHandler)
// The handler should be initialized with correct base URL and API key
// We can't directly test the parent class constructor, but we can verify the handler works
expect(handler).toBeDefined()
})
})
describe("createMessage", () => {
beforeEach(() => {
handler = new RooHandler(mockOptions)
})
it("should handle streaming responses", async () => {
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
expect(chunks.length).toBeGreaterThan(0)
const textChunks = chunks.filter((chunk) => chunk.type === "text")
expect(textChunks).toHaveLength(1)
expect(textChunks[0].text).toBe("Test response")
})
it("should include usage information", async () => {
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
expect(usageChunks).toHaveLength(1)
expect(usageChunks[0].inputTokens).toBe(10)
expect(usageChunks[0].outputTokens).toBe(5)
})
it("should handle API errors", async () => {
mockCreate.mockRejectedValueOnce(new Error("API Error"))
const stream = handler.createMessage(systemPrompt, messages)
await expect(async () => {
for await (const _chunk of stream) {
// Should not reach here
}
}).rejects.toThrow("API Error")
})
it("should handle empty response content", async () => {
mockCreate.mockResolvedValueOnce({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [
{
delta: { content: null },
index: 0,
},
],
usage: {
prompt_tokens: 10,
completion_tokens: 0,
total_tokens: 10,
},
}
},
})
const stream = handler.createMessage(systemPrompt, messages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
const textChunks = chunks.filter((chunk) => chunk.type === "text")
expect(textChunks).toHaveLength(0)
const usageChunks = chunks.filter((chunk) => chunk.type === "usage")
expect(usageChunks).toHaveLength(1)
})
it("should handle multiple messages in conversation", async () => {
const multipleMessages: Anthropic.Messages.MessageParam[] = [
{ role: "user", content: "First message" },
{ role: "assistant", content: "First response" },
{ role: "user", content: "Second message" },
]
const stream = handler.createMessage(systemPrompt, multipleMessages)
const chunks: any[] = []
for await (const chunk of stream) {
chunks.push(chunk)
}
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
messages: expect.arrayContaining([
expect.objectContaining({ role: "system", content: systemPrompt }),
expect.objectContaining({ role: "user", content: "First message" }),
expect.objectContaining({ role: "assistant", content: "First response" }),
expect.objectContaining({ role: "user", content: "Second message" }),
]),
}),
)
})
})
describe("completePrompt", () => {
beforeEach(() => {
handler = new RooHandler(mockOptions)
})
it("should complete prompt successfully", async () => {
const result = await handler.completePrompt("Test prompt")
expect(result).toBe("Test response")
expect(mockCreate).toHaveBeenCalledWith({
model: mockOptions.apiModelId,
messages: [{ role: "user", content: "Test prompt" }],
})
})
it("should handle API errors", async () => {
mockCreate.mockRejectedValueOnce(new Error("API Error"))
await expect(handler.completePrompt("Test prompt")).rejects.toThrow(
"Roo Code Cloud completion error: API Error",
)
})
it("should handle empty response", async () => {
mockCreate.mockResolvedValueOnce({
choices: [{ message: { content: "" } }],
})
const result = await handler.completePrompt("Test prompt")
expect(result).toBe("")
})
it("should handle missing response content", async () => {
mockCreate.mockResolvedValueOnce({
choices: [{ message: {} }],
})
const result = await handler.completePrompt("Test prompt")
expect(result).toBe("")
})
})
describe("getModel", () => {
beforeEach(() => {
handler = new RooHandler(mockOptions)
})
it("should return model info for specified model", () => {
const modelInfo = handler.getModel()
expect(modelInfo.id).toBe(mockOptions.apiModelId)
expect(modelInfo.info).toBeDefined()
// roo/sonic is a valid model in rooModels
expect(modelInfo.info).toBe(rooModels["roo/sonic"])
})
it("should return default model when no model specified", () => {
const handlerWithoutModel = new RooHandler({})
const modelInfo = handlerWithoutModel.getModel()
expect(modelInfo.id).toBe(rooDefaultModelId)
expect(modelInfo.info).toBeDefined()
expect(modelInfo.info).toBe(rooModels[rooDefaultModelId])
})
it("should handle unknown model ID with fallback info", () => {
const handlerWithUnknownModel = new RooHandler({
apiModelId: "unknown-model-id",
})
const modelInfo = handlerWithUnknownModel.getModel()
expect(modelInfo.id).toBe("unknown-model-id")
expect(modelInfo.info).toBeDefined()
// Should return fallback info for unknown models
expect(modelInfo.info.maxTokens).toBe(16_384)
expect(modelInfo.info.contextWindow).toBe(262_144)
expect(modelInfo.info.supportsImages).toBe(false)
expect(modelInfo.info.supportsPromptCache).toBe(true)
expect(modelInfo.info.inputPrice).toBe(0)
expect(modelInfo.info.outputPrice).toBe(0)
})
it("should return correct model info for all Roo models", () => {
// Test each model in rooModels
const modelIds = Object.keys(rooModels) as Array<keyof typeof rooModels>
for (const modelId of modelIds) {
const handlerWithModel = new RooHandler({ apiModelId: modelId })
const modelInfo = handlerWithModel.getModel()
expect(modelInfo.id).toBe(modelId)
expect(modelInfo.info).toBe(rooModels[modelId])
}
})
})
describe("temperature and model configuration", () => {
it("should use default temperature of 0.7", async () => {
handler = new RooHandler(mockOptions)
const stream = handler.createMessage(systemPrompt, messages)
for await (const _chunk of stream) {
// Consume stream
}
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
temperature: 0.7,
}),
)
})
it("should respect custom temperature setting", async () => {
handler = new RooHandler({
...mockOptions,
modelTemperature: 0.9,
})
const stream = handler.createMessage(systemPrompt, messages)
for await (const _chunk of stream) {
// Consume stream
}
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
temperature: 0.9,
}),
)
})
it("should use correct API endpoint", () => {
// The base URL should be set to Roo's API endpoint
// We can't directly test the OpenAI client configuration, but we can verify the handler initializes
handler = new RooHandler(mockOptions)
expect(handler).toBeInstanceOf(RooHandler)
// The handler should work with the Roo API endpoint
})
})
describe("authentication flow", () => {
it("should use session token as API key", () => {
const testToken = "test-session-token-123"
mockGetSessionTokenFn.mockReturnValue(testToken)
handler = new RooHandler(mockOptions)
expect(handler).toBeInstanceOf(RooHandler)
expect(mockGetSessionTokenFn).toHaveBeenCalled()
})
it("should handle undefined auth service", () => {
mockHasInstanceFn.mockReturnValue(true)
// Mock CloudService with undefined authService
const originalGetter = Object.getOwnPropertyDescriptor(CloudService, "instance")?.get
try {
Object.defineProperty(CloudService, "instance", {
get: () => ({ authService: undefined }),
configurable: true,
})
expect(() => {
new RooHandler(mockOptions)
}).toThrow("Authentication required for Roo Code Cloud")
} finally {
// Always restore original getter, even if test fails
if (originalGetter) {
Object.defineProperty(CloudService, "instance", {
get: originalGetter,
configurable: true,
})
}
}
})
it("should handle empty session token", () => {
mockGetSessionTokenFn.mockReturnValue("")
expect(() => {
new RooHandler(mockOptions)
}).toThrow("Authentication required for Roo Code Cloud")
})
})
})

View file

@ -62,11 +62,11 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
})
}
override async *createMessage(
protected createStream(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
) {
const {
id: model,
info: { maxTokens: max_tokens },
@ -83,7 +83,15 @@ export abstract class BaseOpenAiCompatibleProvider<ModelName extends string>
stream_options: { include_usage: true },
}
const stream = await this.client.chat.completions.create(params)
return this.client.chat.completions.create(params)
}
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

View file

@ -0,0 +1,108 @@
import {
DEEP_SEEK_DEFAULT_TEMPERATURE,
type FeatherlessModelId,
featherlessDefaultModelId,
featherlessModels,
} from "@roo-code/types"
import { Anthropic } from "@anthropic-ai/sdk"
import OpenAI from "openai"
import type { ApiHandlerOptions } from "../../shared/api"
import { XmlMatcher } from "../../utils/xml-matcher"
import { convertToR1Format } from "../transform/r1-format"
import { convertToOpenAiMessages } from "../transform/openai-format"
import { ApiStream } from "../transform/stream"
import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider"
export class FeatherlessHandler extends BaseOpenAiCompatibleProvider<FeatherlessModelId> {
constructor(options: ApiHandlerOptions) {
super({
...options,
providerName: "Featherless",
baseURL: "https://api.featherless.ai/v1",
apiKey: options.featherlessApiKey,
defaultProviderModelId: featherlessDefaultModelId,
providerModels: featherlessModels,
defaultTemperature: 0.5,
})
}
private getCompletionParams(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
): OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming {
const {
id: model,
info: { maxTokens: max_tokens },
} = this.getModel()
const temperature = this.options.modelTemperature ?? this.getModel().info.temperature
return {
model,
max_tokens,
temperature,
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
}
}
override async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
const model = this.getModel()
if (model.id.includes("DeepSeek-R1")) {
const stream = await this.client.chat.completions.create({
...this.getCompletionParams(systemPrompt, messages),
messages: convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]),
})
const matcher = new XmlMatcher(
"think",
(chunk) =>
({
type: chunk.matched ? "reasoning" : "text",
text: chunk.data,
}) as const,
)
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
if (delta?.content) {
for (const processedChunk of matcher.update(delta.content)) {
yield processedChunk
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
// Process any remaining content
for (const processedChunk of matcher.final()) {
yield processedChunk
}
} else {
yield* super.createMessage(systemPrompt, messages)
}
}
override getModel() {
const model = super.getModel()
const isDeepSeekR1 = model.id.includes("DeepSeek-R1")
return {
...model,
info: {
...model.info,
temperature: isDeepSeekR1 ? DEEP_SEEK_DEFAULT_TEMPERATURE : this.defaultTemperature,
},
}
}
}

View file

@ -143,6 +143,228 @@ describe("LMStudio Fetcher", () => {
expect(result).toEqual({ [mockRawModel.modelKey]: expectedParsedModel })
})
it("should deduplicate models when both downloaded and loaded", async () => {
const mockDownloadedModel: LLMInfo = {
type: "llm" as const,
modelKey: "mistralai/devstral-small-2505",
format: "safetensors",
displayName: "Devstral Small 2505",
path: "mistralai/devstral-small-2505",
sizeBytes: 13277565112,
architecture: "mistral",
vision: false,
trainedForToolUse: false,
maxContextLength: 131072,
}
const mockLoadedModel: LLMInstanceInfo = {
type: "llm",
modelKey: "devstral-small-2505", // Different key but should match case-insensitively
format: "safetensors",
displayName: "Devstral Small 2505",
path: "mistralai/devstral-small-2505",
sizeBytes: 13277565112,
architecture: "mistral",
identifier: "mistralai/devstral-small-2505",
instanceReference: "RAP5qbeHVjJgBiGFQ6STCuTJ",
vision: false,
trainedForToolUse: false,
maxContextLength: 131072,
contextLength: 7161, // Runtime context info
}
mockedAxios.get.mockResolvedValueOnce({ data: { status: "ok" } })
mockListDownloadedModels.mockResolvedValueOnce([mockDownloadedModel])
mockListLoaded.mockResolvedValueOnce([{ getModelInfo: vi.fn().mockResolvedValueOnce(mockLoadedModel) }])
const result = await getLMStudioModels(baseUrl)
// Should only have one model, with the loaded model replacing the downloaded one
expect(Object.keys(result)).toHaveLength(1)
// The loaded model's key should be used, with loaded model's data
const expectedParsedModel = parseLMStudioModel(mockLoadedModel)
expect(result[mockLoadedModel.modelKey]).toEqual(expectedParsedModel)
// The downloaded model should have been removed
expect(result[mockDownloadedModel.path]).toBeUndefined()
})
it("should handle deduplication with path-based matching", async () => {
const mockDownloadedModel: LLMInfo = {
type: "llm" as const,
modelKey: "Meta/Llama-3.1/8B-Instruct",
format: "gguf",
displayName: "Llama 3.1 8B Instruct",
path: "Meta/Llama-3.1/8B-Instruct",
sizeBytes: 8000000000,
architecture: "llama",
vision: false,
trainedForToolUse: false,
maxContextLength: 8192,
}
const mockLoadedModel: LLMInstanceInfo = {
type: "llm",
modelKey: "Llama-3.1", // Should match the path segment
format: "gguf",
displayName: "Llama 3.1",
path: "Meta/Llama-3.1/8B-Instruct",
sizeBytes: 8000000000,
architecture: "llama",
identifier: "Meta/Llama-3.1/8B-Instruct",
instanceReference: "ABC123",
vision: false,
trainedForToolUse: false,
maxContextLength: 8192,
contextLength: 4096,
}
mockedAxios.get.mockResolvedValueOnce({ data: { status: "ok" } })
mockListDownloadedModels.mockResolvedValueOnce([mockDownloadedModel])
mockListLoaded.mockResolvedValueOnce([{ getModelInfo: vi.fn().mockResolvedValueOnce(mockLoadedModel) }])
const result = await getLMStudioModels(baseUrl)
expect(Object.keys(result)).toHaveLength(1)
expect(result[mockLoadedModel.modelKey]).toBeDefined()
expect(result[mockDownloadedModel.path]).toBeUndefined()
})
it("should not deduplicate models with similar but distinct names", async () => {
const mockDownloadedModels: LLMInfo[] = [
{
type: "llm" as const,
modelKey: "mistral-7b",
format: "gguf",
displayName: "Mistral 7B",
path: "mistralai/mistral-7b-instruct",
sizeBytes: 7000000000,
architecture: "mistral",
vision: false,
trainedForToolUse: false,
maxContextLength: 4096,
},
{
type: "llm" as const,
modelKey: "codellama",
format: "gguf",
displayName: "Code Llama",
path: "meta/codellama/7b",
sizeBytes: 7000000000,
architecture: "llama",
vision: false,
trainedForToolUse: false,
maxContextLength: 4096,
},
]
const mockLoadedModel: LLMInstanceInfo = {
type: "llm",
modelKey: "llama", // Should not match "codellama" or "mistral-7b"
format: "gguf",
displayName: "Llama",
path: "meta/llama/7b",
sizeBytes: 7000000000,
architecture: "llama",
identifier: "meta/llama/7b",
instanceReference: "XYZ789",
vision: false,
trainedForToolUse: false,
maxContextLength: 4096,
contextLength: 2048,
}
mockedAxios.get.mockResolvedValueOnce({ data: { status: "ok" } })
mockListDownloadedModels.mockResolvedValueOnce(mockDownloadedModels)
mockListLoaded.mockResolvedValueOnce([{ getModelInfo: vi.fn().mockResolvedValueOnce(mockLoadedModel) }])
const result = await getLMStudioModels(baseUrl)
// Should have 3 models: mistral-7b (not deduped), codellama (not deduped), and llama (loaded)
expect(Object.keys(result)).toHaveLength(3)
expect(result["mistralai/mistral-7b-instruct"]).toBeDefined() // Should NOT be removed
expect(result["meta/codellama/7b"]).toBeDefined() // Should NOT be removed (codellama != llama)
expect(result[mockLoadedModel.modelKey]).toBeDefined()
})
it("should handle multiple loaded models with various duplicate scenarios", async () => {
const mockDownloadedModels: LLMInfo[] = [
{
type: "llm" as const,
modelKey: "mistral-7b",
format: "gguf",
displayName: "Mistral 7B",
path: "mistralai/mistral-7b/instruct",
sizeBytes: 7000000000,
architecture: "mistral",
vision: false,
trainedForToolUse: false,
maxContextLength: 8192,
},
{
type: "llm" as const,
modelKey: "llama-3.1",
format: "gguf",
displayName: "Llama 3.1",
path: "meta/llama-3.1/8b",
sizeBytes: 8000000000,
architecture: "llama",
vision: false,
trainedForToolUse: false,
maxContextLength: 8192,
},
]
const mockLoadedModels: LLMInstanceInfo[] = [
{
type: "llm",
modelKey: "mistral-7b", // Exact match with path segment
format: "gguf",
displayName: "Mistral 7B",
path: "mistralai/mistral-7b/instruct",
sizeBytes: 7000000000,
architecture: "mistral",
identifier: "mistralai/mistral-7b/instruct",
instanceReference: "REF1",
vision: false,
trainedForToolUse: false,
maxContextLength: 8192,
contextLength: 4096,
},
{
type: "llm",
modelKey: "gpt-4", // No match, new model
format: "gguf",
displayName: "GPT-4",
path: "openai/gpt-4",
sizeBytes: 10000000000,
architecture: "gpt",
identifier: "openai/gpt-4",
instanceReference: "REF2",
vision: true,
trainedForToolUse: true,
maxContextLength: 32768,
contextLength: 16384,
},
]
mockedAxios.get.mockResolvedValueOnce({ data: { status: "ok" } })
mockListDownloadedModels.mockResolvedValueOnce(mockDownloadedModels)
mockListLoaded.mockResolvedValueOnce(
mockLoadedModels.map((model) => ({ getModelInfo: vi.fn().mockResolvedValueOnce(model) })),
)
const result = await getLMStudioModels(baseUrl)
// Should have 3 models: llama-3.1 (downloaded), mistral-7b (loaded, replaced), gpt-4 (loaded, new)
expect(Object.keys(result)).toHaveLength(3)
expect(result["meta/llama-3.1/8b"]).toBeDefined() // Downloaded, not replaced
expect(result["mistralai/mistral-7b/instruct"]).toBeUndefined() // Downloaded, replaced
expect(result["mistral-7b"]).toBeDefined() // Loaded, replaced downloaded
expect(result["gpt-4"]).toBeDefined() // Loaded, new
})
it("should use default baseUrl if an empty string is provided", async () => {
const defaultBaseUrl = "http://localhost:1234"
const defaultLmsUrl = "ws://localhost:1234"

View file

@ -81,12 +81,38 @@ export async function getLMStudioModels(baseUrl = "http://localhost:1234"): Prom
} catch (error) {
console.warn("Failed to list downloaded models, falling back to loaded models only")
}
// We want to list loaded models *anyway* since they provide valuable extra info (context size)
// Get loaded models for their runtime info (context size)
const loadedModels = (await client.llm.listLoaded().then((models: LLM[]) => {
return Promise.all(models.map((m) => m.getModelInfo()))
})) as Array<LLMInstanceInfo>
// Deduplicate: For each loaded model, check if any downloaded model path contains the loaded model's key
// This handles cases like loaded "llama-3.1" matching downloaded "Meta/Llama-3.1/Something"
// If found, remove the downloaded version and add the loaded model (prefer loaded over downloaded for accurate runtime info)
for (const lmstudioModel of loadedModels) {
const loadedModelId = lmstudioModel.modelKey.toLowerCase()
// Find if any downloaded model path contains the loaded model's key as a path segment
// Use word boundaries or path separators to avoid false matches like "llama" matching "codellama"
const existingKey = Object.keys(models).find((key) => {
const keyLower = key.toLowerCase()
// Check if the loaded model ID appears as a distinct segment in the path
// This matches "llama-3.1" in "Meta/Llama-3.1/Something" but not "llama" in "codellama"
return (
keyLower.includes(`/${loadedModelId}/`) ||
keyLower.includes(`/${loadedModelId}`) ||
keyLower.startsWith(`${loadedModelId}/`) ||
keyLower === loadedModelId
)
})
if (existingKey) {
// Remove the downloaded version
delete models[existingKey]
}
// Add the loaded model (either as replacement or new entry)
models[lmstudioModel.modelKey] = parseLMStudioModel(lmstudioModel)
modelsWithLoadedDetails.add(lmstudioModel.modelKey)
}

View file

@ -29,3 +29,5 @@ export { VsCodeLmHandler } from "./vscode-lm"
export { XAIHandler } from "./xai"
export { ZAiHandler } from "./zai"
export { FireworksHandler } from "./fireworks"
export { RooHandler } from "./roo"
export { FeatherlessHandler } from "./featherless"

View file

@ -0,0 +1,285 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { Message, Ollama } from "ollama"
import { ModelInfo, openAiModelInfoSaneDefaults, DEEP_SEEK_DEFAULT_TEMPERATURE } from "@roo-code/types"
import { ApiStream } from "../transform/stream"
import { BaseProvider } from "./base-provider"
import type { ApiHandlerOptions } from "../../shared/api"
import { getOllamaModels } from "./fetchers/ollama"
import { XmlMatcher } from "../../utils/xml-matcher"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
function convertToOllamaMessages(anthropicMessages: Anthropic.Messages.MessageParam[]): Message[] {
const ollamaMessages: Message[] = []
for (const anthropicMessage of anthropicMessages) {
if (typeof anthropicMessage.content === "string") {
ollamaMessages.push({
role: anthropicMessage.role,
content: anthropicMessage.content,
})
} else {
if (anthropicMessage.role === "user") {
const { nonToolMessages, toolMessages } = anthropicMessage.content.reduce<{
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
toolMessages: Anthropic.ToolResultBlockParam[]
}>(
(acc, part) => {
if (part.type === "tool_result") {
acc.toolMessages.push(part)
} else if (part.type === "text" || part.type === "image") {
acc.nonToolMessages.push(part)
}
return acc
},
{ nonToolMessages: [], toolMessages: [] },
)
// Process tool result messages FIRST since they must follow the tool use messages
const toolResultImages: string[] = []
toolMessages.forEach((toolMessage) => {
// The Anthropic SDK allows tool results to be a string or an array of text and image blocks, enabling rich and structured content. In contrast, the Ollama SDK only supports tool results as a single string, so we map the Anthropic tool result parts into one concatenated string to maintain compatibility.
let content: string
if (typeof toolMessage.content === "string") {
content = toolMessage.content
} else {
content =
toolMessage.content
?.map((part) => {
if (part.type === "image") {
// Handle base64 images only (Anthropic SDK uses base64)
// Ollama expects raw base64 strings, not data URLs
if ("source" in part && part.source.type === "base64") {
toolResultImages.push(part.source.data)
}
return "(see following user message for image)"
}
return part.text
})
.join("\n") ?? ""
}
ollamaMessages.push({
role: "user",
images: toolResultImages.length > 0 ? toolResultImages : undefined,
content: content,
})
})
// Process non-tool messages
if (nonToolMessages.length > 0) {
// Separate text and images for Ollama
const textContent = nonToolMessages
.filter((part) => part.type === "text")
.map((part) => part.text)
.join("\n")
const imageData: string[] = []
nonToolMessages.forEach((part) => {
if (part.type === "image" && "source" in part && part.source.type === "base64") {
// Ollama expects raw base64 strings, not data URLs
imageData.push(part.source.data)
}
})
ollamaMessages.push({
role: "user",
content: textContent,
images: imageData.length > 0 ? imageData : undefined,
})
}
} else if (anthropicMessage.role === "assistant") {
const { nonToolMessages } = anthropicMessage.content.reduce<{
nonToolMessages: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[]
toolMessages: Anthropic.ToolUseBlockParam[]
}>(
(acc, part) => {
if (part.type === "tool_use") {
acc.toolMessages.push(part)
} else if (part.type === "text" || part.type === "image") {
acc.nonToolMessages.push(part)
} // assistant cannot send tool_result messages
return acc
},
{ nonToolMessages: [], toolMessages: [] },
)
// Process non-tool messages
let content: string = ""
if (nonToolMessages.length > 0) {
content = nonToolMessages
.map((part) => {
if (part.type === "image") {
return "" // impossible as the assistant cannot send images
}
return part.text
})
.join("\n")
}
ollamaMessages.push({
role: "assistant",
content,
})
}
}
}
return ollamaMessages
}
export class NativeOllamaHandler extends BaseProvider implements SingleCompletionHandler {
protected options: ApiHandlerOptions
private client: Ollama | undefined
protected models: Record<string, ModelInfo> = {}
constructor(options: ApiHandlerOptions) {
super()
this.options = options
}
private ensureClient(): Ollama {
if (!this.client) {
try {
this.client = new Ollama({
host: this.options.ollamaBaseUrl || "http://localhost:11434",
// Note: The ollama npm package handles timeouts internally
})
} catch (error: any) {
throw new Error(`Error creating Ollama client: ${error.message}`)
}
}
return this.client
}
override async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: ApiHandlerCreateMessageMetadata,
): ApiStream {
const client = this.ensureClient()
const { id: modelId, info: modelInfo } = await this.fetchModel()
const useR1Format = modelId.toLowerCase().includes("deepseek-r1")
const ollamaMessages: Message[] = [
{ role: "system", content: systemPrompt },
...convertToOllamaMessages(messages),
]
const matcher = new XmlMatcher(
"think",
(chunk) =>
({
type: chunk.matched ? "reasoning" : "text",
text: chunk.data,
}) as const,
)
try {
// Create the actual API request promise
const stream = await client.chat({
model: modelId,
messages: ollamaMessages,
stream: true,
options: {
num_ctx: modelInfo.contextWindow,
temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0),
},
})
let totalInputTokens = 0
let totalOutputTokens = 0
try {
for await (const chunk of stream) {
if (typeof chunk.message.content === "string") {
// Process content through matcher for reasoning detection
for (const matcherChunk of matcher.update(chunk.message.content)) {
yield matcherChunk
}
}
// Handle token usage if available
if (chunk.eval_count !== undefined || chunk.prompt_eval_count !== undefined) {
if (chunk.prompt_eval_count) {
totalInputTokens = chunk.prompt_eval_count
}
if (chunk.eval_count) {
totalOutputTokens = chunk.eval_count
}
}
}
// Yield any remaining content from the matcher
for (const chunk of matcher.final()) {
yield chunk
}
// Yield usage information if available
if (totalInputTokens > 0 || totalOutputTokens > 0) {
yield {
type: "usage",
inputTokens: totalInputTokens,
outputTokens: totalOutputTokens,
}
}
} catch (streamError: any) {
console.error("Error processing Ollama stream:", streamError)
throw new Error(`Ollama stream processing error: ${streamError.message || "Unknown error"}`)
}
} catch (error: any) {
// Enhance error reporting
const statusCode = error.status || error.statusCode
const errorMessage = error.message || "Unknown error"
if (error.code === "ECONNREFUSED") {
throw new Error(
`Ollama service is not running at ${this.options.ollamaBaseUrl || "http://localhost:11434"}. Please start Ollama first.`,
)
} else if (statusCode === 404) {
throw new Error(
`Model ${this.getModel().id} not found in Ollama. Please pull the model first with: ollama pull ${this.getModel().id}`,
)
}
console.error(`Ollama API error (${statusCode || "unknown"}): ${errorMessage}`)
throw error
}
}
async fetchModel() {
this.models = await getOllamaModels(this.options.ollamaBaseUrl)
return this.getModel()
}
override getModel(): { id: string; info: ModelInfo } {
const modelId = this.options.ollamaModelId || ""
return {
id: modelId,
info: this.models[modelId] || openAiModelInfoSaneDefaults,
}
}
async completePrompt(prompt: string): Promise<string> {
try {
const client = this.ensureClient()
const { id: modelId } = await this.fetchModel()
const useR1Format = modelId.toLowerCase().includes("deepseek-r1")
const response = await client.chat({
model: modelId,
messages: [{ role: "user", content: prompt }],
stream: false,
options: {
temperature: this.options.modelTemperature ?? (useR1Format ? DEEP_SEEK_DEFAULT_TEMPERATURE : 0),
},
})
return response.message?.content || ""
} catch (error) {
if (error instanceof Error) {
throw new Error(`Ollama completion error: ${error.message}`)
}
throw error
}
}
}

93
src/api/providers/roo.ts Normal file
View file

@ -0,0 +1,93 @@
import { Anthropic } from "@anthropic-ai/sdk"
import { rooDefaultModelId, rooModels, type RooModelId } from "@roo-code/types"
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"))
}
super({
...options,
providerName: "Roo Code Cloud",
baseURL: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy/v1",
apiKey: sessionToken,
defaultProviderModelId: rooDefaultModelId,
providerModels: rooModels,
defaultTemperature: 0.7,
})
}
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) {
if (delta.content) {
yield {
type: "text",
text: delta.content,
}
}
if ("reasoning_content" in delta && typeof delta.reasoning_content === "string") {
yield {
type: "reasoning",
text: delta.reasoning_content,
}
}
}
if (chunk.usage) {
yield {
type: "usage",
inputTokens: chunk.usage.prompt_tokens || 0,
outputTokens: chunk.usage.completion_tokens || 0,
}
}
}
}
override getModel() {
const modelId = this.options.apiModelId || rooDefaultModelId
const modelInfo = this.providerModels[modelId as RooModelId] ?? this.providerModels[rooDefaultModelId]
if (modelInfo) {
return { id: modelId as RooModelId, info: modelInfo }
}
// Return the requested model ID even if not found, with fallback info.
return {
id: modelId as RooModelId,
info: {
maxTokens: 16_384,
contextWindow: 262_144,
supportsImages: false,
supportsPromptCache: true,
inputPrice: 0,
outputPrice: 0,
},
}
}
}

View file

@ -10,6 +10,8 @@ import type { ToolParamName, ToolResponse } from "../../shared/tools"
import { fetchInstructionsTool } from "../tools/fetchInstructionsTool"
import { listFilesTool } from "../tools/listFilesTool"
import { getReadFileToolDescription, readFileTool } from "../tools/readFileTool"
import { getSimpleReadFileToolDescription, simpleReadFileTool } from "../tools/simpleReadFileTool"
import { shouldUseSingleFileRead } from "@roo-code/types"
import { writeToFileTool } from "../tools/writeToFileTool"
import { applyDiffTool } from "../tools/multiApplyDiffTool"
import { insertContentTool } from "../tools/insertContentTool"
@ -155,7 +157,13 @@ export async function presentAssistantMessage(cline: Task) {
case "execute_command":
return `[${block.name} for '${block.params.command}']`
case "read_file":
return getReadFileToolDescription(block.name, block.params)
// Check if this model should use the simplified description
const modelId = cline.api.getModel().id
if (shouldUseSingleFileRead(modelId)) {
return getSimpleReadFileToolDescription(block.name, block.params)
} else {
return getReadFileToolDescription(block.name, block.params)
}
case "fetch_instructions":
return `[${block.name} for '${block.params.task}']`
case "write_to_file":
@ -454,8 +462,20 @@ export async function presentAssistantMessage(cline: Task) {
await searchAndReplaceTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
break
case "read_file":
await readFileTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
// Check if this model should use the simplified single-file read tool
const modelId = cline.api.getModel().id
if (shouldUseSingleFileRead(modelId)) {
await simpleReadFileTool(
cline,
block,
askApproval,
handleError,
pushToolResult,
removeClosingTag,
)
} else {
await readFileTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
}
break
case "fetch_instructions":
await fetchInstructionsTool(cline, block, askApproval, handleError, pushToolResult)

View file

@ -270,29 +270,22 @@ Examples:
</search_and_replace>
## ask_followup_question
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- follow_up: (required) A list of 2-4 suggested answers that logically follow from the question, ordered by priority or logical sequence. Each suggestion must:
1. Be provided in its own <suggest> tag
2. Be specific, actionable, and directly related to the completed task
3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses.
4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: <suggest mode="mode-slug">suggestion text</suggest>
- When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge
- question: (required) A clear, specific question addressing the information needed
- follow_up: (required) A list of 2-4 suggested answers, each in its own <suggest> tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
Usage:
<ask_followup_question>
<question>Your question here</question>
<follow_up>
<suggest>
Your suggested answer here
</suggest>
<suggest mode="code">
Implement the solution
</suggest>
<suggest>First suggestion</suggest>
<suggest mode="code">Action with mode switch</suggest>
</follow_up>
</ask_followup_question>
Example: Requesting to ask the user for the path to the frontend-config.json file
Example:
<ask_followup_question>
<question>What is the path to the frontend-config.json file?</question>
<follow_up>
@ -302,16 +295,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
</follow_up>
</ask_followup_question>
Example: Asking a question with mode switching options
<ask_followup_question>
<question>How would you like to proceed with this task?</question>
<follow_up>
<suggest mode="code">Start implementing the solution</suggest>
<suggest mode="architect">Plan the architecture first</suggest>
<suggest>Continue with more details</suggest>
</follow_up>
</ask_followup_question>
## attempt_completion
Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.

View file

@ -167,29 +167,22 @@ Examples:
</list_code_definition_names>
## ask_followup_question
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- follow_up: (required) A list of 2-4 suggested answers that logically follow from the question, ordered by priority or logical sequence. Each suggestion must:
1. Be provided in its own <suggest> tag
2. Be specific, actionable, and directly related to the completed task
3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses.
4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: <suggest mode="mode-slug">suggestion text</suggest>
- When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge
- question: (required) A clear, specific question addressing the information needed
- follow_up: (required) A list of 2-4 suggested answers, each in its own <suggest> tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
Usage:
<ask_followup_question>
<question>Your question here</question>
<follow_up>
<suggest>
Your suggested answer here
</suggest>
<suggest mode="code">
Implement the solution
</suggest>
<suggest>First suggestion</suggest>
<suggest mode="code">Action with mode switch</suggest>
</follow_up>
</ask_followup_question>
Example: Requesting to ask the user for the path to the frontend-config.json file
Example:
<ask_followup_question>
<question>What is the path to the frontend-config.json file?</question>
<follow_up>
@ -199,16 +192,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
</follow_up>
</ask_followup_question>
Example: Asking a question with mode switching options
<ask_followup_question>
<question>How would you like to proceed with this task?</question>
<follow_up>
<suggest mode="code">Start implementing the solution</suggest>
<suggest mode="architect">Plan the architecture first</suggest>
<suggest>Continue with more details</suggest>
</follow_up>
</ask_followup_question>
## attempt_completion
Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.

View file

@ -269,29 +269,22 @@ Examples:
</search_and_replace>
## ask_followup_question
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- follow_up: (required) A list of 2-4 suggested answers that logically follow from the question, ordered by priority or logical sequence. Each suggestion must:
1. Be provided in its own <suggest> tag
2. Be specific, actionable, and directly related to the completed task
3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses.
4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: <suggest mode="mode-slug">suggestion text</suggest>
- When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge
- question: (required) A clear, specific question addressing the information needed
- follow_up: (required) A list of 2-4 suggested answers, each in its own <suggest> tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
Usage:
<ask_followup_question>
<question>Your question here</question>
<follow_up>
<suggest>
Your suggested answer here
</suggest>
<suggest mode="code">
Implement the solution
</suggest>
<suggest>First suggestion</suggest>
<suggest mode="code">Action with mode switch</suggest>
</follow_up>
</ask_followup_question>
Example: Requesting to ask the user for the path to the frontend-config.json file
Example:
<ask_followup_question>
<question>What is the path to the frontend-config.json file?</question>
<follow_up>
@ -301,16 +294,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
</follow_up>
</ask_followup_question>
Example: Asking a question with mode switching options
<ask_followup_question>
<question>How would you like to proceed with this task?</question>
<follow_up>
<suggest mode="code">Start implementing the solution</suggest>
<suggest mode="architect">Plan the architecture first</suggest>
<suggest>Continue with more details</suggest>
</follow_up>
</ask_followup_question>
## attempt_completion
Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.

View file

@ -319,29 +319,22 @@ Example: Requesting to access an MCP resource
</access_mcp_resource>
## ask_followup_question
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- follow_up: (required) A list of 2-4 suggested answers that logically follow from the question, ordered by priority or logical sequence. Each suggestion must:
1. Be provided in its own <suggest> tag
2. Be specific, actionable, and directly related to the completed task
3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses.
4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: <suggest mode="mode-slug">suggestion text</suggest>
- When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge
- question: (required) A clear, specific question addressing the information needed
- follow_up: (required) A list of 2-4 suggested answers, each in its own <suggest> tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
Usage:
<ask_followup_question>
<question>Your question here</question>
<follow_up>
<suggest>
Your suggested answer here
</suggest>
<suggest mode="code">
Implement the solution
</suggest>
<suggest>First suggestion</suggest>
<suggest mode="code">Action with mode switch</suggest>
</follow_up>
</ask_followup_question>
Example: Requesting to ask the user for the path to the frontend-config.json file
Example:
<ask_followup_question>
<question>What is the path to the frontend-config.json file?</question>
<follow_up>
@ -351,16 +344,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
</follow_up>
</ask_followup_question>
Example: Asking a question with mode switching options
<ask_followup_question>
<question>How would you like to proceed with this task?</question>
<follow_up>
<suggest mode="code">Start implementing the solution</suggest>
<suggest mode="architect">Plan the architecture first</suggest>
<suggest>Continue with more details</suggest>
</follow_up>
</ask_followup_question>
## attempt_completion
Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.

View file

@ -275,29 +275,22 @@ Examples:
</search_and_replace>
## ask_followup_question
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- follow_up: (required) A list of 2-4 suggested answers that logically follow from the question, ordered by priority or logical sequence. Each suggestion must:
1. Be provided in its own <suggest> tag
2. Be specific, actionable, and directly related to the completed task
3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses.
4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: <suggest mode="mode-slug">suggestion text</suggest>
- When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge
- question: (required) A clear, specific question addressing the information needed
- follow_up: (required) A list of 2-4 suggested answers, each in its own <suggest> tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
Usage:
<ask_followup_question>
<question>Your question here</question>
<follow_up>
<suggest>
Your suggested answer here
</suggest>
<suggest mode="code">
Implement the solution
</suggest>
<suggest>First suggestion</suggest>
<suggest mode="code">Action with mode switch</suggest>
</follow_up>
</ask_followup_question>
Example: Requesting to ask the user for the path to the frontend-config.json file
Example:
<ask_followup_question>
<question>What is the path to the frontend-config.json file?</question>
<follow_up>
@ -307,16 +300,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
</follow_up>
</ask_followup_question>
Example: Asking a question with mode switching options
<ask_followup_question>
<question>How would you like to proceed with this task?</question>
<follow_up>
<suggest mode="code">Start implementing the solution</suggest>
<suggest mode="architect">Plan the architecture first</suggest>
<suggest>Continue with more details</suggest>
</follow_up>
</ask_followup_question>
## attempt_completion
Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.

View file

@ -270,29 +270,22 @@ Examples:
</search_and_replace>
## ask_followup_question
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- follow_up: (required) A list of 2-4 suggested answers that logically follow from the question, ordered by priority or logical sequence. Each suggestion must:
1. Be provided in its own <suggest> tag
2. Be specific, actionable, and directly related to the completed task
3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses.
4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: <suggest mode="mode-slug">suggestion text</suggest>
- When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge
- question: (required) A clear, specific question addressing the information needed
- follow_up: (required) A list of 2-4 suggested answers, each in its own <suggest> tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
Usage:
<ask_followup_question>
<question>Your question here</question>
<follow_up>
<suggest>
Your suggested answer here
</suggest>
<suggest mode="code">
Implement the solution
</suggest>
<suggest>First suggestion</suggest>
<suggest mode="code">Action with mode switch</suggest>
</follow_up>
</ask_followup_question>
Example: Requesting to ask the user for the path to the frontend-config.json file
Example:
<ask_followup_question>
<question>What is the path to the frontend-config.json file?</question>
<follow_up>
@ -302,16 +295,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
</follow_up>
</ask_followup_question>
Example: Asking a question with mode switching options
<ask_followup_question>
<question>How would you like to proceed with this task?</question>
<follow_up>
<suggest mode="code">Start implementing the solution</suggest>
<suggest mode="architect">Plan the architecture first</suggest>
<suggest>Continue with more details</suggest>
</follow_up>
</ask_followup_question>
## attempt_completion
Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.

View file

@ -323,29 +323,22 @@ Example: Requesting to click on the element at coordinates 450,300
</browser_action>
## ask_followup_question
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- follow_up: (required) A list of 2-4 suggested answers that logically follow from the question, ordered by priority or logical sequence. Each suggestion must:
1. Be provided in its own <suggest> tag
2. Be specific, actionable, and directly related to the completed task
3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses.
4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: <suggest mode="mode-slug">suggestion text</suggest>
- When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge
- question: (required) A clear, specific question addressing the information needed
- follow_up: (required) A list of 2-4 suggested answers, each in its own <suggest> tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
Usage:
<ask_followup_question>
<question>Your question here</question>
<follow_up>
<suggest>
Your suggested answer here
</suggest>
<suggest mode="code">
Implement the solution
</suggest>
<suggest>First suggestion</suggest>
<suggest mode="code">Action with mode switch</suggest>
</follow_up>
</ask_followup_question>
Example: Requesting to ask the user for the path to the frontend-config.json file
Example:
<ask_followup_question>
<question>What is the path to the frontend-config.json file?</question>
<follow_up>
@ -355,16 +348,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
</follow_up>
</ask_followup_question>
Example: Asking a question with mode switching options
<ask_followup_question>
<question>How would you like to proceed with this task?</question>
<follow_up>
<suggest mode="code">Start implementing the solution</suggest>
<suggest mode="architect">Plan the architecture first</suggest>
<suggest>Continue with more details</suggest>
</follow_up>
</ask_followup_question>
## attempt_completion
Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.

View file

@ -270,29 +270,22 @@ Examples:
</search_and_replace>
## ask_followup_question
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- follow_up: (required) A list of 2-4 suggested answers that logically follow from the question, ordered by priority or logical sequence. Each suggestion must:
1. Be provided in its own <suggest> tag
2. Be specific, actionable, and directly related to the completed task
3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses.
4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: <suggest mode="mode-slug">suggestion text</suggest>
- When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge
- question: (required) A clear, specific question addressing the information needed
- follow_up: (required) A list of 2-4 suggested answers, each in its own <suggest> tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
Usage:
<ask_followup_question>
<question>Your question here</question>
<follow_up>
<suggest>
Your suggested answer here
</suggest>
<suggest mode="code">
Implement the solution
</suggest>
<suggest>First suggestion</suggest>
<suggest mode="code">Action with mode switch</suggest>
</follow_up>
</ask_followup_question>
Example: Requesting to ask the user for the path to the frontend-config.json file
Example:
<ask_followup_question>
<question>What is the path to the frontend-config.json file?</question>
<follow_up>
@ -302,16 +295,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
</follow_up>
</ask_followup_question>
Example: Asking a question with mode switching options
<ask_followup_question>
<question>How would you like to proceed with this task?</question>
<follow_up>
<suggest mode="code">Start implementing the solution</suggest>
<suggest mode="architect">Plan the architecture first</suggest>
<suggest>Continue with more details</suggest>
</follow_up>
</ask_followup_question>
## attempt_completion
Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.

View file

@ -358,29 +358,22 @@ Examples:
</search_and_replace>
## ask_followup_question
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- follow_up: (required) A list of 2-4 suggested answers that logically follow from the question, ordered by priority or logical sequence. Each suggestion must:
1. Be provided in its own <suggest> tag
2. Be specific, actionable, and directly related to the completed task
3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses.
4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: <suggest mode="mode-slug">suggestion text</suggest>
- When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge
- question: (required) A clear, specific question addressing the information needed
- follow_up: (required) A list of 2-4 suggested answers, each in its own <suggest> tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
Usage:
<ask_followup_question>
<question>Your question here</question>
<follow_up>
<suggest>
Your suggested answer here
</suggest>
<suggest mode="code">
Implement the solution
</suggest>
<suggest>First suggestion</suggest>
<suggest mode="code">Action with mode switch</suggest>
</follow_up>
</ask_followup_question>
Example: Requesting to ask the user for the path to the frontend-config.json file
Example:
<ask_followup_question>
<question>What is the path to the frontend-config.json file?</question>
<follow_up>
@ -390,16 +383,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
</follow_up>
</ask_followup_question>
Example: Asking a question with mode switching options
<ask_followup_question>
<question>How would you like to proceed with this task?</question>
<follow_up>
<suggest mode="code">Start implementing the solution</suggest>
<suggest mode="architect">Plan the architecture first</suggest>
<suggest>Continue with more details</suggest>
</follow_up>
</ask_followup_question>
## attempt_completion
Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.

View file

@ -270,29 +270,22 @@ Examples:
</search_and_replace>
## ask_followup_question
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- follow_up: (required) A list of 2-4 suggested answers that logically follow from the question, ordered by priority or logical sequence. Each suggestion must:
1. Be provided in its own <suggest> tag
2. Be specific, actionable, and directly related to the completed task
3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses.
4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: <suggest mode="mode-slug">suggestion text</suggest>
- When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge
- question: (required) A clear, specific question addressing the information needed
- follow_up: (required) A list of 2-4 suggested answers, each in its own <suggest> tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
Usage:
<ask_followup_question>
<question>Your question here</question>
<follow_up>
<suggest>
Your suggested answer here
</suggest>
<suggest mode="code">
Implement the solution
</suggest>
<suggest>First suggestion</suggest>
<suggest mode="code">Action with mode switch</suggest>
</follow_up>
</ask_followup_question>
Example: Requesting to ask the user for the path to the frontend-config.json file
Example:
<ask_followup_question>
<question>What is the path to the frontend-config.json file?</question>
<follow_up>
@ -302,16 +295,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
</follow_up>
</ask_followup_question>
Example: Asking a question with mode switching options
<ask_followup_question>
<question>How would you like to proceed with this task?</question>
<follow_up>
<suggest mode="code">Start implementing the solution</suggest>
<suggest mode="architect">Plan the architecture first</suggest>
<suggest>Continue with more details</suggest>
</follow_up>
</ask_followup_question>
## attempt_completion
Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.

View file

@ -323,29 +323,22 @@ Example: Requesting to click on the element at coordinates 450,300
</browser_action>
## ask_followup_question
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- follow_up: (required) A list of 2-4 suggested answers that logically follow from the question, ordered by priority or logical sequence. Each suggestion must:
1. Be provided in its own <suggest> tag
2. Be specific, actionable, and directly related to the completed task
3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses.
4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: <suggest mode="mode-slug">suggestion text</suggest>
- When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge
- question: (required) A clear, specific question addressing the information needed
- follow_up: (required) A list of 2-4 suggested answers, each in its own <suggest> tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
Usage:
<ask_followup_question>
<question>Your question here</question>
<follow_up>
<suggest>
Your suggested answer here
</suggest>
<suggest mode="code">
Implement the solution
</suggest>
<suggest>First suggestion</suggest>
<suggest mode="code">Action with mode switch</suggest>
</follow_up>
</ask_followup_question>
Example: Requesting to ask the user for the path to the frontend-config.json file
Example:
<ask_followup_question>
<question>What is the path to the frontend-config.json file?</question>
<follow_up>
@ -355,16 +348,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
</follow_up>
</ask_followup_question>
Example: Asking a question with mode switching options
<ask_followup_question>
<question>How would you like to proceed with this task?</question>
<follow_up>
<suggest mode="code">Start implementing the solution</suggest>
<suggest mode="architect">Plan the architecture first</suggest>
<suggest>Continue with more details</suggest>
</follow_up>
</ask_followup_question>
## attempt_completion
Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.

View file

@ -319,29 +319,22 @@ Example: Requesting to access an MCP resource
</access_mcp_resource>
## ask_followup_question
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- follow_up: (required) A list of 2-4 suggested answers that logically follow from the question, ordered by priority or logical sequence. Each suggestion must:
1. Be provided in its own <suggest> tag
2. Be specific, actionable, and directly related to the completed task
3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses.
4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: <suggest mode="mode-slug">suggestion text</suggest>
- When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge
- question: (required) A clear, specific question addressing the information needed
- follow_up: (required) A list of 2-4 suggested answers, each in its own <suggest> tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
Usage:
<ask_followup_question>
<question>Your question here</question>
<follow_up>
<suggest>
Your suggested answer here
</suggest>
<suggest mode="code">
Implement the solution
</suggest>
<suggest>First suggestion</suggest>
<suggest mode="code">Action with mode switch</suggest>
</follow_up>
</ask_followup_question>
Example: Requesting to ask the user for the path to the frontend-config.json file
Example:
<ask_followup_question>
<question>What is the path to the frontend-config.json file?</question>
<follow_up>
@ -351,16 +344,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
</follow_up>
</ask_followup_question>
Example: Asking a question with mode switching options
<ask_followup_question>
<question>How would you like to proceed with this task?</question>
<follow_up>
<suggest mode="code">Start implementing the solution</suggest>
<suggest mode="architect">Plan the architecture first</suggest>
<suggest>Continue with more details</suggest>
</follow_up>
</ask_followup_question>
## attempt_completion
Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.

View file

@ -270,29 +270,22 @@ Examples:
</search_and_replace>
## ask_followup_question
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- follow_up: (required) A list of 2-4 suggested answers that logically follow from the question, ordered by priority or logical sequence. Each suggestion must:
1. Be provided in its own <suggest> tag
2. Be specific, actionable, and directly related to the completed task
3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses.
4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: <suggest mode="mode-slug">suggestion text</suggest>
- When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge
- question: (required) A clear, specific question addressing the information needed
- follow_up: (required) A list of 2-4 suggested answers, each in its own <suggest> tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
Usage:
<ask_followup_question>
<question>Your question here</question>
<follow_up>
<suggest>
Your suggested answer here
</suggest>
<suggest mode="code">
Implement the solution
</suggest>
<suggest>First suggestion</suggest>
<suggest mode="code">Action with mode switch</suggest>
</follow_up>
</ask_followup_question>
Example: Requesting to ask the user for the path to the frontend-config.json file
Example:
<ask_followup_question>
<question>What is the path to the frontend-config.json file?</question>
<follow_up>
@ -302,16 +295,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
</follow_up>
</ask_followup_question>
Example: Asking a question with mode switching options
<ask_followup_question>
<question>How would you like to proceed with this task?</question>
<follow_up>
<suggest mode="code">Start implementing the solution</suggest>
<suggest mode="architect">Plan the architecture first</suggest>
<suggest>Continue with more details</suggest>
</follow_up>
</ask_followup_question>
## attempt_completion
Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must ask yourself in <thinking></thinking> tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool.

View file

@ -61,6 +61,7 @@ async function generatePrompt(
partialReadsEnabled?: boolean,
settings?: SystemPromptSettings,
todoList?: TodoItem[],
modelId?: string,
): Promise<string> {
if (!context) {
throw new Error("Extension context is required for generating system prompt")
@ -106,6 +107,7 @@ ${getToolDescriptionsForMode(
partialReadsEnabled,
settings,
enableMcpServerCreation,
modelId,
)}
${getToolUseGuidelinesSection(codeIndexManager)}
@ -150,6 +152,7 @@ export const SYSTEM_PROMPT = async (
partialReadsEnabled?: boolean,
settings?: SystemPromptSettings,
todoList?: TodoItem[],
modelId?: string,
): Promise<string> => {
if (!context) {
throw new Error("Extension context is required for generating system prompt")
@ -221,5 +224,6 @@ ${customInstructions}`
partialReadsEnabled,
settings,
todoList,
modelId,
)
}

View file

@ -1,28 +1,21 @@
export function getAskFollowupQuestionDescription(): string {
return `## ask_followup_question
Description: Ask the user a question to gather additional information needed to complete the task. This tool should be used when you encounter ambiguities, need clarification, or require more details to proceed effectively. It allows for interactive problem-solving by enabling direct communication with the user. Use this tool judiciously to maintain a balance between gathering necessary information and avoiding excessive back-and-forth.
Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
Parameters:
- question: (required) The question to ask the user. This should be a clear, specific question that addresses the information you need.
- follow_up: (required) A list of 2-4 suggested answers that logically follow from the question, ordered by priority or logical sequence. Each suggestion must:
1. Be provided in its own <suggest> tag
2. Be specific, actionable, and directly related to the completed task
3. Be a complete answer to the question - the user should not need to provide additional information or fill in any missing details. DO NOT include placeholders with brackets or parentheses.
4. Optionally include a mode attribute to switch to a specific mode when the suggestion is selected: <suggest mode="mode-slug">suggestion text</suggest>
- When using the mode attribute, focus the suggestion text on the action to be taken rather than mentioning the mode switch, as the mode change is handled automatically and indicated by a visual badge
- question: (required) A clear, specific question addressing the information needed
- follow_up: (required) A list of 2-4 suggested answers, each in its own <suggest> tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
Usage:
<ask_followup_question>
<question>Your question here</question>
<follow_up>
<suggest>
Your suggested answer here
</suggest>
<suggest mode="code">
Implement the solution
</suggest>
<suggest>First suggestion</suggest>
<suggest mode="code">Action with mode switch</suggest>
</follow_up>
</ask_followup_question>
Example: Requesting to ask the user for the path to the frontend-config.json file
Example:
<ask_followup_question>
<question>What is the path to the frontend-config.json file?</question>
<follow_up>
@ -30,15 +23,5 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil
<suggest>./config/frontend-config.json</suggest>
<suggest>./frontend-config.json</suggest>
</follow_up>
</ask_followup_question>
Example: Asking a question with mode switching options
<ask_followup_question>
<question>How would you like to proceed with this task?</question>
<follow_up>
<suggest mode="code">Start implementing the solution</suggest>
<suggest mode="architect">Plan the architecture first</suggest>
<suggest>Continue with more details</suggest>
</follow_up>
</ask_followup_question>`
}

View file

@ -7,7 +7,9 @@ import { Mode, getModeConfig, isToolAllowedForMode, getGroupName } from "../../.
import { ToolArgs } from "./types"
import { getExecuteCommandDescription } from "./execute-command"
import { getReadFileDescription } from "./read-file"
import { getSimpleReadFileDescription } from "./simple-read-file"
import { getFetchInstructionsDescription } from "./fetch-instructions"
import { shouldUseSingleFileRead } from "@roo-code/types"
import { getWriteToFileDescription } from "./write-to-file"
import { getSearchFilesDescription } from "./search-files"
import { getListFilesDescription } from "./list-files"
@ -28,7 +30,14 @@ import { CodeIndexManager } from "../../../services/code-index/manager"
// Map of tool names to their description functions
const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined> = {
execute_command: (args) => getExecuteCommandDescription(args),
read_file: (args) => getReadFileDescription(args),
read_file: (args) => {
// Check if the current model should use the simplified read_file tool
const modelId = args.settings?.modelId
if (modelId && shouldUseSingleFileRead(modelId)) {
return getSimpleReadFileDescription(args)
}
return getReadFileDescription(args)
},
fetch_instructions: (args) => getFetchInstructionsDescription(args.settings?.enableMcpServerCreation),
write_to_file: (args) => getWriteToFileDescription(args),
search_files: (args) => getSearchFilesDescription(args),
@ -62,6 +71,7 @@ export function getToolDescriptionsForMode(
partialReadsEnabled?: boolean,
settings?: Record<string, any>,
enableMcpServerCreation?: boolean,
modelId?: string,
): string {
const config = getModeConfig(mode, customModes)
const args: ToolArgs = {
@ -74,6 +84,7 @@ export function getToolDescriptionsForMode(
settings: {
...settings,
enableMcpServerCreation,
modelId,
},
experiments,
}
@ -138,6 +149,7 @@ export function getToolDescriptionsForMode(
export {
getExecuteCommandDescription,
getReadFileDescription,
getSimpleReadFileDescription,
getFetchInstructionsDescription,
getWriteToFileDescription,
getSearchFilesDescription,

View file

@ -0,0 +1,35 @@
import { ToolArgs } from "./types"
/**
* Generate a simplified read_file tool description for models that only support single file reads
* Uses the simpler format: <read_file><path>file/path.ext</path></read_file>
*/
export function getSimpleReadFileDescription(args: ToolArgs): string {
return `## read_file
Description: Request to read the contents of a file. The tool outputs line-numbered content (e.g. "1 | const x = 1") for easy reference when discussing code.
Parameters:
- path: (required) File path (relative to workspace directory ${args.cwd})
Usage:
<read_file>
<path>path/to/file</path>
</read_file>
Examples:
1. Reading a TypeScript file:
<read_file>
<path>src/app.ts</path>
</read_file>
2. Reading a configuration file:
<read_file>
<path>config.json</path>
</read_file>
3. Reading a markdown file:
<read_file>
<path>README.md</path>
</read_file>`
}

View file

@ -21,16 +21,18 @@ import {
type ClineMessage,
type ClineSay,
type ClineAsk,
type BlockingAsk,
type ToolProgressStatus,
type HistoryItem,
RooCodeEventName,
TelemetryEventName,
TaskStatus,
TodoItem,
DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
getApiProtocol,
getModelId,
DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
isBlockingAsk,
isIdleAsk,
isInteractiveAsk,
isResumableAsk,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { CloudService, ExtensionBridgeService } from "@roo-code/cloud"
@ -182,7 +184,12 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
providerRef: WeakRef<ClineProvider>
private readonly globalStoragePath: string
abort: boolean = false
blockingAsk?: BlockingAsk
// TaskStatus
idleAsk?: ClineMessage
resumableAsk?: ClineMessage
interactiveAsk?: ClineMessage
didFinishAbortingStream = false
abandoned = false
isInitialized = false
@ -290,7 +297,6 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.taskId = historyItem ? historyItem.id : crypto.randomUUID()
this.metadata = {
taskId: this.taskId,
task: historyItem ? historyItem.task : task,
images: historyItem ? [] : images,
}
@ -497,6 +503,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
if (this._taskMode === undefined) {
throw new Error("Task mode accessed before initialization. Use getTaskMode() or wait for taskModeReady.")
}
return this._taskMode
}
@ -615,6 +622,16 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
}
private findMessageByTimestamp(ts: number): ClineMessage | undefined {
for (let i = this.clineMessages.length - 1; i >= 0; i--) {
if (this.clineMessages[i].ts === ts) {
return this.clineMessages[i]
}
}
return undefined
}
// Note that `partial` has three valid states true (partial message),
// false (completion of partial message), undefined (individual complete
// message).
@ -713,16 +730,55 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, isProtected })
}
// Detect if the task will enter an idle state.
const isReady = this.askResponse !== undefined || this.lastMessageTs !== askTs
// The state is mutable if the message is complete and the task will
// block (via the `pWaitFor`).
const isBlocking = !(this.askResponse !== undefined || this.lastMessageTs !== askTs)
const isStatusMutable = !partial && isBlocking
let statusMutationTimeouts: NodeJS.Timeout[] = []
if (!partial && !isReady && isBlockingAsk(type)) {
this.blockingAsk = type
this.emit(RooCodeEventName.TaskIdle, this.taskId)
if (isStatusMutable) {
if (isInteractiveAsk(type)) {
statusMutationTimeouts.push(
setTimeout(() => {
const message = this.findMessageByTimestamp(askTs)
if (message) {
this.interactiveAsk = message
this.emit(RooCodeEventName.TaskInteractive, this.taskId)
}
}, 1_000),
)
} else if (isResumableAsk(type)) {
statusMutationTimeouts.push(
setTimeout(() => {
const message = this.findMessageByTimestamp(askTs)
if (message) {
this.resumableAsk = message
this.emit(RooCodeEventName.TaskResumable, this.taskId)
}
}, 1_000),
)
} else if (isIdleAsk(type)) {
statusMutationTimeouts.push(
setTimeout(() => {
const message = this.findMessageByTimestamp(askTs)
if (message) {
this.idleAsk = message
this.emit(RooCodeEventName.TaskIdle, this.taskId)
}
}, 1_000),
)
}
}
console.log(`[Task#${this.taskId}] pWaitFor askResponse(${type}) -> blocking`)
console.log(
`[Task#${this.taskId}] pWaitFor askResponse(${type}) -> blocking (isStatusMutable = ${isStatusMutable}, statusMutationTimeouts = ${statusMutationTimeouts.length})`,
)
await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })
console.log(`[Task#${this.taskId}] pWaitFor askResponse(${type}) -> unblocked (${this.askResponse})`)
if (this.lastMessageTs !== askTs) {
@ -737,9 +793,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.askResponseText = undefined
this.askResponseImages = undefined
// Cancel the timeouts if they are still running.
statusMutationTimeouts.forEach((timeout) => clearTimeout(timeout))
// Switch back to an active state.
if (this.blockingAsk) {
this.blockingAsk = undefined
if (this.idleAsk || this.resumableAsk || this.interactiveAsk) {
this.idleAsk = undefined
this.resumableAsk = undefined
this.interactiveAsk = undefined
this.emit(RooCodeEventName.TaskActive, this.taskId)
}
@ -757,23 +818,27 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.askResponseImages = images
}
public approveAsk({ text, images }: { text?: string; images?: string[] } = {}) {
this.handleWebviewAskResponse("yesButtonClicked", text, images)
}
public denyAsk({ text, images }: { text?: string; images?: string[] } = {}) {
this.handleWebviewAskResponse("noButtonClicked", text, images)
}
public submitUserMessage(text: string, images?: string[], modeSlug?: string): void {
try {
const trimmed = (text ?? "").trim()
const imgs = images ?? []
text = (text ?? "").trim()
images = images ?? []
const provider = this.providerRef.deref()
if (!provider) {
console.error("[Task#submitUserMessage] Provider reference lost")
return
}
// Run asynchronously to allow awaiting mode switch before sending the message
void (async () => {
// If a mode slug is provided, handle the mode switch first (same behavior as createTask)
try {
const modeSlugValue = (modeSlug ?? "").trim()
if (modeSlugValue.length > 0) {
if (modeSlugValue.length > 0 && provider) {
const customModes = await provider.customModesManager.getCustomModes()
const targetMode = getModeBySlug(modeSlugValue, customModes)
if (targetMode) {
@ -784,7 +849,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
}
} catch (err) {
provider.log(
provider?.log(
`[Task#submitUserMessage] Failed to apply modeSlug: ${
err instanceof Error ? err.message : String(err)
}`,
@ -792,16 +857,20 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
// If there's no content to send, exit after potential mode switch
if (!trimmed && imgs.length === 0) {
if (text.length === 0 && images.length === 0) {
return
}
await provider.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: trimmed,
images: imgs,
})
if (provider) {
await provider.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text,
images,
})
} else {
console.error("[Task#submitUserMessage] Provider reference lost")
}
})()
} catch (error) {
console.error("[Task#submitUserMessage] Failed to submit user message:", error)
@ -1055,12 +1124,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
public async resumePausedTask(lastMessage: string) {
// Release this Cline instance from paused state.
this.isPaused = false
this.emit(RooCodeEventName.TaskUnpaused)
// Fake an answer from the subtask that it has completed running and
// this is the result of what it has done add the message to the chat
// this is the result of what it has done add the message to the chat
// history and to the webview ui.
try {
await this.say("subtask_result", lastMessage)
@ -2188,6 +2256,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
todoListEnabled: apiConfiguration?.todoListEnabled ?? true,
useAgentRules: vscode.workspace.getConfiguration("roo-cline").get<boolean>("useAgentRules") ?? true,
},
undefined, // todoList
this.api.getModel().id,
)
})()
}
@ -2545,4 +2615,24 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
public get cwd() {
return this.workspacePath
}
public get taskStatus(): TaskStatus {
if (this.interactiveAsk) {
return TaskStatus.Interactive
}
if (this.resumableAsk) {
return TaskStatus.Resumable
}
if (this.idleAsk) {
return TaskStatus.Idle
}
return TaskStatus.Running
}
public get taskAsk(): ClineMessage | undefined {
return this.idleAsk || this.resumableAsk || this.interactiveAsk
}
}

View file

@ -1614,4 +1614,103 @@ describe("Cline", () => {
})
})
})
describe("abortTask", () => {
it("should set abort flag and emit TaskAborted event", async () => {
const task = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
startTask: false,
})
// Spy on emit method
const emitSpy = vi.spyOn(task, "emit")
// Mock the dispose method to avoid actual cleanup
vi.spyOn(task, "dispose").mockImplementation(() => {})
// Call abortTask
await task.abortTask()
// Verify abort flag is set
expect(task.abort).toBe(true)
// Verify TaskAborted event was emitted
expect(emitSpy).toHaveBeenCalledWith("taskAborted")
})
it("should be equivalent to clicking Cancel button functionality", async () => {
const task = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
startTask: false,
})
// Mock the dispose method to track cleanup
const disposeSpy = vi.spyOn(task, "dispose").mockImplementation(() => {})
// Call abortTask
await task.abortTask()
// Verify the same behavior as Cancel button
expect(task.abort).toBe(true)
expect(disposeSpy).toHaveBeenCalled()
})
it("should work with TaskLike interface", async () => {
const task = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
startTask: false,
})
// Cast to TaskLike to ensure interface compliance
const taskLike = task as any // TaskLike interface from types package
// Verify abortTask method exists and is callable
expect(typeof taskLike.abortTask).toBe("function")
// Mock the dispose method to avoid actual cleanup
vi.spyOn(task, "dispose").mockImplementation(() => {})
// Call abortTask through interface
await taskLike.abortTask()
// Verify it works
expect(task.abort).toBe(true)
})
it("should handle errors during disposal gracefully", async () => {
const task = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "test task",
startTask: false,
})
// Mock dispose to throw an error
const mockError = new Error("Disposal failed")
vi.spyOn(task, "dispose").mockImplementation(() => {
throw mockError
})
// Spy on console.error to verify error is logged
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
// abortTask should not throw even if dispose fails
await expect(task.abortTask()).resolves.not.toThrow()
// Verify error was logged
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining("Error during task"), mockError)
// Verify abort flag is still set
expect(task.abort).toBe(true)
// Restore console.error
consoleErrorSpy.mockRestore()
})
})
})

View file

@ -213,12 +213,7 @@ describe("executeCommand", () => {
// Verify
expect(rejected).toBe(false)
expect(TerminalRegistry.getOrCreateTerminal).toHaveBeenCalledWith(
customCwd,
true, // customCwd provided
mockTask.taskId,
"vscode",
)
expect(TerminalRegistry.getOrCreateTerminal).toHaveBeenCalledWith(customCwd, mockTask.taskId, "vscode")
expect(result).toContain(`within working directory '${customCwd}'`)
})
@ -248,12 +243,7 @@ describe("executeCommand", () => {
// Verify
expect(rejected).toBe(false)
expect(TerminalRegistry.getOrCreateTerminal).toHaveBeenCalledWith(
resolvedCwd,
true, // customCwd provided
mockTask.taskId,
"vscode",
)
expect(TerminalRegistry.getOrCreateTerminal).toHaveBeenCalledWith(resolvedCwd, mockTask.taskId, "vscode")
expect(result).toContain(`within working directory '${resolvedCwd.toPosix()}'`)
})
@ -302,12 +292,7 @@ describe("executeCommand", () => {
await executeCommand(mockTask, options)
// Verify
expect(TerminalRegistry.getOrCreateTerminal).toHaveBeenCalledWith(
mockTask.cwd,
false, // no customCwd
mockTask.taskId,
"vscode",
)
expect(TerminalRegistry.getOrCreateTerminal).toHaveBeenCalledWith(mockTask.cwd, mockTask.taskId, "vscode")
})
it("should use execa provider when shell integration is disabled", async () => {
@ -330,12 +315,7 @@ describe("executeCommand", () => {
await executeCommand(mockTask, options)
// Verify
expect(TerminalRegistry.getOrCreateTerminal).toHaveBeenCalledWith(
mockTask.cwd,
false, // no customCwd
mockTask.taskId,
"execa",
)
expect(TerminalRegistry.getOrCreateTerminal).toHaveBeenCalledWith(mockTask.cwd, mockTask.taskId, "execa")
})
})

View file

@ -238,7 +238,7 @@ export async function executeCommand(
}
}
const terminal = await TerminalRegistry.getOrCreateTerminal(workingDir, !!customCwd, task.taskId, terminalProvider)
const terminal = await TerminalRegistry.getOrCreateTerminal(workingDir, task.taskId, terminalProvider)
if (terminal instanceof Terminal) {
terminal.terminal.show(true)

View file

@ -0,0 +1,287 @@
import path from "path"
import { isBinaryFile } from "isbinaryfile"
import { Task } from "../task/Task"
import { ClineSayTool } from "../../shared/ExtensionMessage"
import { formatResponse } from "../prompts/responses"
import { t } from "../../i18n"
import { ToolUse, AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "../../shared/tools"
import { RecordSource } from "../context-tracking/FileContextTrackerTypes"
import { isPathOutsideWorkspace } from "../../utils/pathUtils"
import { getReadablePath } from "../../utils/path"
import { countFileLines } from "../../integrations/misc/line-counter"
import { readLines } from "../../integrations/misc/read-lines"
import { extractTextFromFile, addLineNumbers, getSupportedBinaryFormats } from "../../integrations/misc/extract-text"
import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter"
import {
DEFAULT_MAX_IMAGE_FILE_SIZE_MB,
DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB,
isSupportedImageFormat,
validateImageForProcessing,
processImageFile,
} from "./helpers/imageHelpers"
/**
* Simplified read file tool for models that only support single file reads
* Uses the format: <read_file><path>file/path.ext</path></read_file>
*
* This is a streamlined version of readFileTool that:
* - Only accepts a single path parameter
* - Does not support multiple files
* - Does not support line ranges
* - Has simpler XML parsing
*/
export async function simpleReadFileTool(
cline: Task,
block: ToolUse,
askApproval: AskApproval,
handleError: HandleError,
pushToolResult: PushToolResult,
_removeClosingTag: RemoveClosingTag,
) {
const filePath: string | undefined = block.params.path
// Check if the current model supports images
const modelInfo = cline.api.getModel().info
const supportsImages = modelInfo.supportsImages ?? false
// Handle partial message
if (block.partial) {
const fullPath = filePath ? path.resolve(cline.cwd, filePath) : ""
const sharedMessageProps: ClineSayTool = {
tool: "readFile",
path: getReadablePath(cline.cwd, filePath || ""),
isOutsideWorkspace: filePath ? isPathOutsideWorkspace(fullPath) : false,
}
const partialMessage = JSON.stringify({
...sharedMessageProps,
content: undefined,
} satisfies ClineSayTool)
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
return
}
// Validate path parameter
if (!filePath) {
cline.consecutiveMistakeCount++
cline.recordToolError("read_file")
const errorMsg = await cline.sayAndCreateMissingParamError("read_file", "path")
pushToolResult(`<file><error>${errorMsg}</error></file>`)
return
}
const relPath = filePath
const fullPath = path.resolve(cline.cwd, relPath)
try {
// Check RooIgnore validation
const accessAllowed = cline.rooIgnoreController?.validateAccess(relPath)
if (!accessAllowed) {
await cline.say("rooignore_error", relPath)
const errorMsg = formatResponse.rooIgnoreError(relPath)
pushToolResult(`<file><path>${relPath}</path><error>${errorMsg}</error></file>`)
return
}
// Get max read file line setting
const { maxReadFileLine = -1 } = (await cline.providerRef.deref()?.getState()) ?? {}
// Create approval message
const isOutsideWorkspace = isPathOutsideWorkspace(fullPath)
let lineSnippet = ""
if (maxReadFileLine === 0) {
lineSnippet = t("tools:readFile.definitionsOnly")
} else if (maxReadFileLine > 0) {
lineSnippet = t("tools:readFile.maxLines", { max: maxReadFileLine })
}
const completeMessage = JSON.stringify({
tool: "readFile",
path: getReadablePath(cline.cwd, relPath),
isOutsideWorkspace,
content: fullPath,
reason: lineSnippet,
} satisfies ClineSayTool)
const { response, text, images } = await cline.ask("tool", completeMessage, false)
if (response !== "yesButtonClicked") {
// Handle denial
if (text) {
await cline.say("user_feedback", text, images)
}
cline.didRejectTool = true
const statusMessage = text ? formatResponse.toolDeniedWithFeedback(text) : formatResponse.toolDenied()
pushToolResult(`${statusMessage}\n<file><path>${relPath}</path><status>Denied by user</status></file>`)
return
}
// Handle approval with feedback
if (text) {
await cline.say("user_feedback", text, images)
}
// Process the file
const [totalLines, isBinary] = await Promise.all([countFileLines(fullPath), isBinaryFile(fullPath)])
// Handle binary files
if (isBinary) {
const fileExtension = path.extname(relPath).toLowerCase()
const supportedBinaryFormats = getSupportedBinaryFormats()
// Check if it's a supported image format
if (isSupportedImageFormat(fileExtension)) {
try {
const {
maxImageFileSize = DEFAULT_MAX_IMAGE_FILE_SIZE_MB,
maxTotalImageSize = DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB,
} = (await cline.providerRef.deref()?.getState()) ?? {}
// Validate image for processing
const validationResult = await validateImageForProcessing(
fullPath,
supportsImages,
maxImageFileSize,
maxTotalImageSize,
0, // No cumulative memory for single file
)
if (!validationResult.isValid) {
await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource)
pushToolResult(
`<file><path>${relPath}</path>\n<notice>${validationResult.notice}</notice>\n</file>`,
)
return
}
// Process the image
const imageResult = await processImageFile(fullPath)
await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource)
// Return result with image data
const result = formatResponse.toolResult(
`<file><path>${relPath}</path>\n<notice>${imageResult.notice}</notice>\n</file>`,
supportsImages ? [imageResult.dataUrl] : undefined,
)
if (typeof result === "string") {
pushToolResult(result)
} else {
pushToolResult(result)
}
return
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
pushToolResult(
`<file><path>${relPath}</path><error>Error reading image file: ${errorMsg}</error></file>`,
)
await handleError(
`reading image file ${relPath}`,
error instanceof Error ? error : new Error(errorMsg),
)
return
}
}
// Check if it's a supported binary format that can be processed
if (supportedBinaryFormats && supportedBinaryFormats.includes(fileExtension)) {
// For supported binary formats (.pdf, .docx, .ipynb), continue to extractTextFromFile
// Fall through to the normal extractTextFromFile processing below
} else {
// Handle unknown binary format
const fileFormat = fileExtension.slice(1) || "bin"
pushToolResult(
`<file><path>${relPath}</path>\n<binary_file format="${fileFormat}">Binary file - content not displayed</binary_file>\n</file>`,
)
return
}
}
// Handle definitions-only mode
if (maxReadFileLine === 0) {
try {
const defResult = await parseSourceCodeDefinitionsForFile(fullPath, cline.rooIgnoreController)
if (defResult) {
let xmlInfo = `<notice>Showing only definitions. Use standard read_file if you need to read actual content</notice>\n`
pushToolResult(
`<file><path>${relPath}</path>\n<list_code_definition_names>${defResult}</list_code_definition_names>\n${xmlInfo}</file>`,
)
}
} catch (error) {
if (error instanceof Error && error.message.startsWith("Unsupported language:")) {
console.warn(`[simple_read_file] Warning: ${error.message}`)
} else {
console.error(
`[simple_read_file] Unhandled error: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
return
}
// Handle files exceeding line threshold
if (maxReadFileLine > 0 && totalLines > maxReadFileLine) {
const content = addLineNumbers(await readLines(fullPath, maxReadFileLine - 1, 0))
const lineRangeAttr = ` lines="1-${maxReadFileLine}"`
let xmlInfo = `<content${lineRangeAttr}>\n${content}</content>\n`
try {
const defResult = await parseSourceCodeDefinitionsForFile(fullPath, cline.rooIgnoreController)
if (defResult) {
xmlInfo += `<list_code_definition_names>${defResult}</list_code_definition_names>\n`
}
xmlInfo += `<notice>Showing only ${maxReadFileLine} of ${totalLines} total lines. File is too large for complete display</notice>\n`
pushToolResult(`<file><path>${relPath}</path>\n${xmlInfo}</file>`)
} catch (error) {
if (error instanceof Error && error.message.startsWith("Unsupported language:")) {
console.warn(`[simple_read_file] Warning: ${error.message}`)
} else {
console.error(
`[simple_read_file] Unhandled error: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
return
}
// Handle normal file read
const content = await extractTextFromFile(fullPath)
const lineRangeAttr = ` lines="1-${totalLines}"`
let xmlInfo = totalLines > 0 ? `<content${lineRangeAttr}>\n${content}</content>\n` : `<content/>`
if (totalLines === 0) {
xmlInfo += `<notice>File is empty</notice>\n`
}
// Track file read
await cline.fileContextTracker.trackFileContext(relPath, "read_tool" as RecordSource)
// Return the result
if (text) {
const statusMessage = formatResponse.toolApprovedWithFeedback(text)
pushToolResult(`${statusMessage}\n<file><path>${relPath}</path>\n${xmlInfo}</file>`)
} else {
pushToolResult(`<file><path>${relPath}</path>\n${xmlInfo}</file>`)
}
} catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error)
pushToolResult(`<file><path>${relPath}</path><error>Error reading file: ${errorMsg}</error></file>`)
await handleError(`reading file ${relPath}`, error instanceof Error ? error : new Error(errorMsg))
}
}
/**
* Get description for the simple read file tool
* @param blockName The name of the tool block
* @param blockParams The parameters passed to the tool
* @returns A description string for the tool use
*/
export function getSimpleReadFileToolDescription(blockName: string, blockParams: any): string {
if (blockParams.path) {
return `[${blockName} for '${blockParams.path}']`
} else {
return `[${blockName} with missing path]`
}
}

View file

@ -30,6 +30,7 @@ import {
type TerminalActionPromptType,
type HistoryItem,
type CreateTaskOptions,
type ClineAsk,
RooCodeEventName,
requestyDefaultModelId,
openRouterDefaultModelId,
@ -120,7 +121,7 @@ export class ClineProvider
public isViewLaunched = false
public settingsImportedAt?: number
public readonly latestAnnouncementId = "jul-29-2025-3-25-0" // Update for v3.25.0 announcement
public readonly latestAnnouncementId = "aug-20-2025-stealth-model" // Update for stealth model announcement
public readonly providerSettingsManager: ProviderSettingsManager
public readonly customModesManager: CustomModesManager
@ -177,6 +178,8 @@ export class ClineProvider
const onTaskFocused = () => this.emit(RooCodeEventName.TaskFocused, instance.taskId)
const onTaskUnfocused = () => this.emit(RooCodeEventName.TaskUnfocused, instance.taskId)
const onTaskActive = (taskId: string) => this.emit(RooCodeEventName.TaskActive, taskId)
const onTaskInteractive = (taskId: string) => this.emit(RooCodeEventName.TaskInteractive, taskId)
const onTaskResumable = (taskId: string) => this.emit(RooCodeEventName.TaskResumable, taskId)
const onTaskIdle = (taskId: string) => this.emit(RooCodeEventName.TaskIdle, taskId)
// Attach the listeners.
@ -186,6 +189,8 @@ export class ClineProvider
instance.on(RooCodeEventName.TaskFocused, onTaskFocused)
instance.on(RooCodeEventName.TaskUnfocused, onTaskUnfocused)
instance.on(RooCodeEventName.TaskActive, onTaskActive)
instance.on(RooCodeEventName.TaskInteractive, onTaskInteractive)
instance.on(RooCodeEventName.TaskResumable, onTaskResumable)
instance.on(RooCodeEventName.TaskIdle, onTaskIdle)
// Store the cleanup functions for later removal.
@ -196,6 +201,8 @@ export class ClineProvider
() => instance.off(RooCodeEventName.TaskFocused, onTaskFocused),
() => instance.off(RooCodeEventName.TaskUnfocused, onTaskUnfocused),
() => instance.off(RooCodeEventName.TaskActive, onTaskActive),
() => instance.off(RooCodeEventName.TaskInteractive, onTaskInteractive),
() => instance.off(RooCodeEventName.TaskResumable, onTaskResumable),
() => instance.off(RooCodeEventName.TaskIdle, onTaskIdle),
])
}
@ -404,6 +411,13 @@ export class ClineProvider
await this.removeClineFromStack()
}
resumeTask(taskId: string): void {
// Use the existing showTaskWithId method which handles both current and historical tasks
this.showTaskWithId(taskId).catch((error) => {
this.log(`Failed to resume task ${taskId}: ${error.message}`)
})
}
getRecentTasks(): string[] {
if (this.recentTasksCache) {
return this.recentTasksCache
@ -1881,7 +1895,7 @@ export class ClineProvider
followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 60000,
includeDiagnosticMessages: includeDiagnosticMessages ?? true,
maxDiagnosticMessages: maxDiagnosticMessages ?? 50,
includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? false,
includeTaskHistoryInEnhance: includeTaskHistoryInEnhance ?? true,
remoteControlEnabled: remoteControlEnabled ?? false,
}
}
@ -2069,7 +2083,7 @@ export class ClineProvider
includeDiagnosticMessages: stateValues.includeDiagnosticMessages ?? true,
maxDiagnosticMessages: stateValues.maxDiagnosticMessages ?? 50,
// Add includeTaskHistoryInEnhance setting
includeTaskHistoryInEnhance: stateValues.includeTaskHistoryInEnhance ?? false,
includeTaskHistoryInEnhance: stateValues.includeTaskHistoryInEnhance ?? true,
// Add remoteControlEnabled setting
remoteControlEnabled: stateValues.remoteControlEnabled ?? false,
}

View file

@ -1343,7 +1343,7 @@ export const webviewMessageHandler = async (
await provider.postStateToWebview()
break
case "includeTaskHistoryInEnhance":
await updateGlobalState("includeTaskHistoryInEnhance", message.bool ?? false)
await updateGlobalState("includeTaskHistoryInEnhance", message.bool ?? true)
await provider.postStateToWebview()
break
case "condensingApiConfigId":

View file

@ -78,6 +78,17 @@ export class API extends EventEmitter<RooCodeEvents> implements RooCodeAPI {
await vscode.commands.executeCommand("workbench.action.files.saveFiles")
await vscode.commands.executeCommand("workbench.action.closeWindow")
break
case TaskCommandName.ResumeTask:
this.log(`[API] ResumeTask -> ${data}`)
try {
await this.resumeTask(data)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
this.log(`[API] ResumeTask failed for taskId ${data}: ${errorMessage}`)
// Don't rethrow - we want to prevent IPC server crashes
// The error is logged for debugging purposes
}
break
}
})
}

View file

@ -104,6 +104,9 @@
"noResponseBody": "Error de l'API de Cerebras: No hi ha cos de resposta",
"completionError": "Error de finalització de Cerebras: {{error}}"
},
"roo": {
"authenticationRequired": "El proveïdor Roo requereix autenticació al núvol. Si us plau, inicieu sessió a Roo Code Cloud."
},
"mode_import_failed": "Ha fallat la importació del mode: {{error}}"
},
"warnings": {

View file

@ -100,6 +100,9 @@
"genericError": "Cerebras API-Fehler ({{status}}): {{message}}",
"noResponseBody": "Cerebras API-Fehler: Kein Antworttext vorhanden",
"completionError": "Cerebras-Vervollständigungsfehler: {{error}}"
},
"roo": {
"authenticationRequired": "Roo-Anbieter erfordert Cloud-Authentifizierung. Bitte melde dich bei Roo Code Cloud an."
}
},
"warnings": {

View file

@ -100,6 +100,9 @@
"genericError": "Cerebras API Error ({{status}}): {{message}}",
"noResponseBody": "Cerebras API Error: No response body",
"completionError": "Cerebras completion error: {{error}}"
},
"roo": {
"authenticationRequired": "Roo provider requires cloud authentication. Please sign in to Roo Code Cloud."
}
},
"warnings": {

View file

@ -100,6 +100,9 @@
"genericError": "Error de la API de Cerebras ({{status}}): {{message}}",
"noResponseBody": "Error de la API de Cerebras: Sin cuerpo de respuesta",
"completionError": "Error de finalización de Cerebras: {{error}}"
},
"roo": {
"authenticationRequired": "El proveedor Roo requiere autenticación en la nube. Por favor, inicia sesión en Roo Code Cloud."
}
},
"warnings": {

View file

@ -100,6 +100,9 @@
"genericError": "Erreur de l'API Cerebras ({{status}}) : {{message}}",
"noResponseBody": "Erreur de l'API Cerebras : Aucun corps de réponse",
"completionError": "Erreur d'achèvement de Cerebras : {{error}}"
},
"roo": {
"authenticationRequired": "Le fournisseur Roo nécessite une authentification cloud. Veuillez vous connecter à Roo Code Cloud."
}
},
"warnings": {

View file

@ -100,6 +100,9 @@
"genericError": "Cerebras API त्रुटि ({{status}}): {{message}}",
"noResponseBody": "Cerebras API त्रुटि: कोई प्रतिक्रिया मुख्य भाग नहीं",
"completionError": "Cerebras पूर्णता त्रुटि: {{error}}"
},
"roo": {
"authenticationRequired": "Roo प्रदाता को क्लाउड प्रमाणीकरण की आवश्यकता है। कृपया Roo Code Cloud में साइन इन करें।"
}
},
"warnings": {

View file

@ -100,6 +100,9 @@
"genericError": "Kesalahan API Cerebras ({{status}}): {{message}}",
"noResponseBody": "Kesalahan API Cerebras: Tidak ada isi respons",
"completionError": "Kesalahan penyelesaian Cerebras: {{error}}"
},
"roo": {
"authenticationRequired": "Penyedia Roo memerlukan autentikasi cloud. Silakan masuk ke Roo Code Cloud."
}
},
"warnings": {

View file

@ -100,6 +100,9 @@
"genericError": "Errore API Cerebras ({{status}}): {{message}}",
"noResponseBody": "Errore API Cerebras: Nessun corpo di risposta",
"completionError": "Errore di completamento Cerebras: {{error}}"
},
"roo": {
"authenticationRequired": "Il provider Roo richiede l'autenticazione cloud. Accedi a Roo Code Cloud."
}
},
"warnings": {

View file

@ -100,6 +100,9 @@
"genericError": "Cerebras APIエラー ({{status}}): {{message}}",
"noResponseBody": "Cerebras APIエラー: レスポンスボディなし",
"completionError": "Cerebras完了エラー: {{error}}"
},
"roo": {
"authenticationRequired": "Rooプロバイダーはクラウド認証が必要です。Roo Code Cloudにサインインしてください。"
}
},
"warnings": {

View file

@ -100,6 +100,9 @@
"genericError": "Cerebras API 오류 ({{status}}): {{message}}",
"noResponseBody": "Cerebras API 오류: 응답 본문 없음",
"completionError": "Cerebras 완료 오류: {{error}}"
},
"roo": {
"authenticationRequired": "Roo 제공업체는 클라우드 인증이 필요합니다. Roo Code Cloud에 로그인하세요."
}
},
"warnings": {

View file

@ -100,6 +100,9 @@
"genericError": "Cerebras API-fout ({{status}}): {{message}}",
"noResponseBody": "Cerebras API-fout: Geen responslichaam",
"completionError": "Cerebras-voltooiingsfout: {{error}}"
},
"roo": {
"authenticationRequired": "Roo provider vereist cloud authenticatie. Log in bij Roo Code Cloud."
}
},
"warnings": {

View file

@ -100,6 +100,9 @@
"genericError": "Błąd API Cerebras ({{status}}): {{message}}",
"noResponseBody": "Błąd API Cerebras: Brak treści odpowiedzi",
"completionError": "Błąd uzupełniania Cerebras: {{error}}"
},
"roo": {
"authenticationRequired": "Dostawca Roo wymaga uwierzytelnienia w chmurze. Zaloguj się do Roo Code Cloud."
}
},
"warnings": {

View file

@ -104,6 +104,9 @@
"genericError": "Erro da API Cerebras ({{status}}): {{message}}",
"noResponseBody": "Erro da API Cerebras: Sem corpo de resposta",
"completionError": "Erro de conclusão do Cerebras: {{error}}"
},
"roo": {
"authenticationRequired": "O provedor Roo requer autenticação na nuvem. Faça login no Roo Code Cloud."
}
},
"warnings": {

View file

@ -100,6 +100,9 @@
"genericError": "Ошибка Cerebras API ({{status}}): {{message}}",
"noResponseBody": "Ошибка Cerebras API: Нет тела ответа",
"completionError": "Ошибка завершения Cerebras: {{error}}"
},
"roo": {
"authenticationRequired": "Провайдер Roo требует облачной аутентификации. Войдите в Roo Code Cloud."
}
},
"warnings": {

View file

@ -100,6 +100,9 @@
"genericError": "Cerebras API Hatası ({{status}}): {{message}}",
"noResponseBody": "Cerebras API Hatası: Yanıt gövdesi yok",
"completionError": "Cerebras tamamlama hatası: {{error}}"
},
"roo": {
"authenticationRequired": "Roo sağlayıcısı bulut kimlik doğrulaması gerektirir. Lütfen Roo Code Cloud'a giriş yapın."
}
},
"warnings": {

View file

@ -100,6 +100,9 @@
"genericError": "Lỗi API Cerebras ({{status}}): {{message}}",
"noResponseBody": "Lỗi API Cerebras: Không có nội dung phản hồi",
"completionError": "Lỗi hoàn thành Cerebras: {{error}}"
},
"roo": {
"authenticationRequired": "Nhà cung cấp Roo yêu cầu xác thực đám mây. Vui lòng đăng nhập vào Roo Code Cloud."
}
},
"warnings": {

View file

@ -105,6 +105,9 @@
"genericError": "Cerebras API 错误 ({{status}}){{message}}",
"noResponseBody": "Cerebras API 错误:无响应主体",
"completionError": "Cerebras 完成错误:{{error}}"
},
"roo": {
"authenticationRequired": "Roo 提供商需要云认证。请登录 Roo Code Cloud。"
}
},
"warnings": {

View file

@ -100,6 +100,9 @@
"noResponseBody": "Cerebras API 錯誤:無回應主體",
"completionError": "Cerebras 完成錯誤:{{error}}"
},
"roo": {
"authenticationRequired": "Roo 提供者需要雲端認證。請登入 Roo Code Cloud。"
},
"mode_import_failed": "匯入模式失敗:{{error}}"
},
"warnings": {

View file

@ -146,13 +146,11 @@ export class TerminalRegistry {
* directory.
*
* @param cwd The working directory path
* @param requiredCwd Whether the working directory is required (if false, may reuse any non-busy terminal)
* @param taskId Optional task ID to associate with the terminal
* @returns A Terminal instance
*/
public static async getOrCreateTerminal(
cwd: string,
requiredCwd: boolean = false,
taskId?: string,
provider: RooTerminalProvider = "vscode",
): Promise<RooTerminal> {
@ -194,12 +192,6 @@ export class TerminalRegistry {
})
}
// Third priority: Find any non-busy terminal (only if directory is not
// required).
if (!terminal && !requiredCwd) {
terminal = terminals.find((t) => !t.busy && t.provider === provider)
}
// If no suitable terminal found, create a new one.
if (!terminal) {
terminal = this.createTerminal(cwd, provider)

View file

@ -3,7 +3,7 @@
"displayName": "%extension.displayName%",
"description": "%extension.description%",
"publisher": "RooVeterinaryInc",
"version": "3.25.15",
"version": "3.25.20",
"icon": "assets/icons/icon.png",
"galleryBanner": {
"color": "#617A91",
@ -427,7 +427,7 @@
"@mistralai/mistralai": "^1.3.6",
"@modelcontextprotocol/sdk": "^1.9.0",
"@qdrant/js-client-rest": "^1.14.0",
"@roo-code/cloud": "^0.15.0",
"@roo-code/cloud": "^0.19.0",
"@roo-code/ipc": "workspace:^",
"@roo-code/telemetry": "workspace:^",
"@roo-code/types": "workspace:^",
@ -458,6 +458,7 @@
"monaco-vscode-textmate-theme-converter": "^0.1.7",
"node-cache": "^5.1.2",
"node-ipc": "^12.0.0",
"ollama": "^0.5.17",
"openai": "^5.0.0",
"os-name": "^6.0.0",
"p-limit": "^6.2.0",

View file

@ -70,6 +70,7 @@ export class ProfileValidator {
case "sambanova":
case "chutes":
case "fireworks":
case "featherless":
return profile.apiModelId
case "litellm":
return profile.litellmModelId

View file

@ -195,6 +195,7 @@ describe("ProfileValidator", () => {
"chutes",
"sambanova",
"fireworks",
"featherless",
]
apiModelProviders.forEach((provider) => {

View file

@ -375,12 +375,41 @@ describe("shouldUseReasoningEffort", () => {
reasoningEffort: "medium",
}
// Should return true regardless of settings
// Should return true regardless of settings (unless explicitly disabled)
expect(shouldUseReasoningEffort({ model })).toBe(true)
expect(shouldUseReasoningEffort({ model, settings: {} })).toBe(true)
expect(shouldUseReasoningEffort({ model, settings: { reasoningEffort: undefined } })).toBe(true)
})
test("should return false when enableReasoningEffort is false, even if reasoningEffort is set", () => {
const model: ModelInfo = {
contextWindow: 200_000,
supportsPromptCache: true,
supportsReasoningEffort: true,
}
const settings: ProviderSettings = {
enableReasoningEffort: false,
reasoningEffort: "medium",
}
expect(shouldUseReasoningEffort({ model, settings })).toBe(false)
})
test("should return false when enableReasoningEffort is false, even if model has reasoningEffort property", () => {
const model: ModelInfo = {
contextWindow: 200_000,
supportsPromptCache: true,
reasoningEffort: "medium",
}
const settings: ProviderSettings = {
enableReasoningEffort: false,
}
expect(shouldUseReasoningEffort({ model, settings })).toBe(false)
})
test("should return true when model supports reasoning effort and settings provide reasoning effort", () => {
const model: ModelInfo = {
contextWindow: 200_000,

View file

@ -63,7 +63,17 @@ export const shouldUseReasoningEffort = ({
}: {
model: ModelInfo
settings?: ProviderSettings
}): boolean => (!!model.supportsReasoningEffort && !!settings?.reasoningEffort) || !!model.reasoningEffort
}): boolean => {
// If enableReasoningEffort is explicitly set to false, reasoning should be disabled
if (settings?.enableReasoningEffort === false) {
return false
}
// Otherwise, use reasoning if:
// 1. Model supports reasoning effort AND settings provide reasoning effort, OR
// 2. Model itself has a reasoningEffort property
return (!!model.supportsReasoningEffort && !!settings?.reasoningEffort) || !!model.reasoningEffort
}
export const DEFAULT_HYBRID_REASONING_MODEL_MAX_TOKENS = 16_384
export const DEFAULT_HYBRID_REASONING_MODEL_THINKING_TOKENS = 8_192

View file

@ -5,8 +5,8 @@ export function checkExistKey(config: ProviderSettings | undefined) {
return false
}
// Special case for human-relay, fake-ai, and claude-code providers which don't need any configuration.
if (config.apiProvider && ["human-relay", "fake-ai", "claude-code"].includes(config.apiProvider)) {
// Special case for human-relay, fake-ai, claude-code, and roo providers which don't need any configuration.
if (config.apiProvider && ["human-relay", "fake-ai", "claude-code", "roo"].includes(config.apiProvider)) {
return true
}

View file

@ -3,9 +3,11 @@ import { Trans } from "react-i18next"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { Package } from "@roo/package"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@src/components/ui"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { vscode } from "@src/utils/vscode"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@src/components/ui"
import { Button } from "@src/components/ui"
interface AnnouncementProps {
hideAnnouncement: () => void
@ -23,6 +25,7 @@ interface AnnouncementProps {
const Announcement = ({ hideAnnouncement }: AnnouncementProps) => {
const { t } = useAppTranslation()
const [open, setOpen] = useState(true)
const { cloudIsAuthenticated } = useExtensionState()
return (
<Dialog
@ -37,98 +40,64 @@ const Announcement = ({ hideAnnouncement }: AnnouncementProps) => {
<DialogContent className="max-w-96">
<DialogHeader>
<DialogTitle>{t("chat:announcement.title", { version: Package.version })}</DialogTitle>
<DialogDescription>
{t("chat:announcement.description", { version: Package.version })}
</DialogDescription>
</DialogHeader>
<div>
<h3>{t("chat:announcement.whatsNew")}</h3>
<ul className="space-y-2">
<li>
{" "}
<Trans
i18nKey="chat:announcement.feature1"
i18nKey="chat:announcement.stealthModel.feature"
components={{
bold: <b />,
code: <code />,
settingsLink: (
<VSCodeLink
href="#"
onClick={(e) => {
e.preventDefault()
setOpen(false)
hideAnnouncement()
window.postMessage(
{
type: "action",
action: "settingsButtonClicked",
values: { section: "codebaseIndexing" },
},
"*",
)
}}
/>
),
}}
/>
</li>
<li>
{" "}
<Trans
i18nKey="chat:announcement.feature2"
components={{
bold: <b />,
code: <code />,
}}
/>
</li>
<li>
{" "}
<Trans
i18nKey="chat:announcement.feature3"
components={{
bold: <b />,
code: <code />,
}}
/>
</li>
</ul>
<Trans
i18nKey="chat:announcement.detailsDiscussLinks"
components={{ discordLink: <DiscordLink />, redditLink: <RedditLink /> }}
/>
<p className="text-xs text-muted-foreground mt-2">{t("chat:announcement.stealthModel.note")}</p>
<div className="mt-4">
{!cloudIsAuthenticated ? (
<Button
onClick={() => {
vscode.postMessage({ type: "rooCloudSignIn" })
}}
className="w-full">
{t("chat:announcement.stealthModel.connectButton")}
</Button>
) : (
<div className="text-sm w-full">
<Trans
i18nKey="chat:announcement.stealthModel.selectModel"
components={{
code: <code className="px-1 py-0.5 bg-gray-100 dark:bg-gray-800 rounded" />,
settingsLink: (
<VSCodeLink
href="#"
onClick={(e) => {
e.preventDefault()
setOpen(false)
hideAnnouncement()
window.postMessage(
{
type: "action",
action: "settingsButtonClicked",
values: { section: "provider" },
},
"*",
)
}}
/>
),
}}
/>
</div>
)}
</div>
</div>
</DialogContent>
</Dialog>
)
}
const DiscordLink = () => (
<VSCodeLink
href="https://discord.gg/roocode"
onClick={(e) => {
e.preventDefault()
window.postMessage(
{ type: "action", action: "openExternal", data: { url: "https://discord.gg/roocode" } },
"*",
)
}}>
Discord
</VSCodeLink>
)
const RedditLink = () => (
<VSCodeLink
href="https://reddit.com/r/RooCode"
onClick={(e) => {
e.preventDefault()
window.postMessage(
{ type: "action", action: "openExternal", data: { url: "https://reddit.com/r/RooCode" } },
"*",
)
}}>
Reddit
</VSCodeLink>
)
export default memo(Announcement)

View file

@ -11,17 +11,27 @@ vi.mock("@src/components/ui", () => ({
DialogDescription: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogHeader: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogTitle: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
DialogFooter: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
Button: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => (
<button onClick={onClick}>{children}</button>
),
}))
// Mock the useAppTranslation hook
// Mock the useAppTranslation hook and Trans component
vi.mock("@src/i18n/TranslationContext", () => ({
useAppTranslation: () => ({
t: (key: string, options?: { version: string }) => {
if (key === "chat:announcement.title") {
return `🎉 Roo Code ${options?.version} Released`
}
if (key === "chat:announcement.description") {
return `Roo Code ${options?.version} brings powerful new features and improvements based on your feedback.`
if (key === "chat:announcement.stealthModel.feature") {
return "Stealth reasoning model with advanced capabilities"
}
if (key === "chat:announcement.stealthModel.note") {
return "Note: This is an experimental feature"
}
if (key === "chat:announcement.stealthModel.connectButton") {
return "Connect to Roo Code Cloud"
}
// Return key for other translations not relevant to this test
return key
@ -29,6 +39,34 @@ vi.mock("@src/i18n/TranslationContext", () => ({
}),
}))
// Mock react-i18next Trans component
vi.mock("react-i18next", () => ({
Trans: ({ i18nKey, children }: { i18nKey?: string; children: React.ReactNode }) => {
if (i18nKey === "chat:announcement.stealthModel.feature") {
return <>Stealth reasoning model with advanced capabilities</>
}
if (i18nKey === "chat:announcement.stealthModel.selectModel") {
return <>Please select the roo/sonic model in settings</>
}
return <>{children}</>
},
}))
// Mock VSCodeLink
vi.mock("@vscode/webview-ui-toolkit/react", () => ({
VSCodeLink: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => (
<a onClick={onClick}>{children}</a>
),
}))
// Mock the useExtensionState hook
vi.mock("@src/context/ExtensionStateContext", () => ({
useExtensionState: () => ({
apiConfiguration: null,
cloudIsAuthenticated: false,
}),
}))
describe("Announcement", () => {
const mockHideAnnouncement = vi.fn()
const expectedVersion = Package.version
@ -36,12 +74,16 @@ describe("Announcement", () => {
it("renders the announcement with the version number from package.json", () => {
render(<Announcement hideAnnouncement={mockHideAnnouncement} />)
// Check if the mocked version number is present in the title and description
// Check if the mocked version number is present in the title
expect(screen.getByText(`🎉 Roo Code ${expectedVersion} Released`)).toBeInTheDocument()
expect(
screen.getByText(
`Roo Code ${expectedVersion} brings powerful new features and improvements based on your feedback.`,
),
).toBeInTheDocument()
// Check if the stealth model feature is displayed (using partial match due to bullet point)
expect(screen.getByText(/Stealth reasoning model with advanced capabilities/)).toBeInTheDocument()
// Check if the note is displayed
expect(screen.getByText("Note: This is an experimental feature")).toBeInTheDocument()
// Check if the connect button is displayed (since cloudIsAuthenticated is false in the mock)
expect(screen.getByText("Connect to Roo Code Cloud")).toBeInTheDocument()
})
})

View file

@ -1,7 +1,7 @@
import React, { memo, useCallback, useEffect, useMemo, useState } from "react"
import { convertHeadersToObject } from "./utils/headers"
import { useDebounce } from "react-use"
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
import { VSCodeLink, VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { ExternalLinkIcon } from "@radix-ui/react-icons"
import {
@ -31,7 +31,9 @@ import {
internationalZAiDefaultModelId,
mainlandZAiDefaultModelId,
fireworksDefaultModelId,
featherlessDefaultModelId,
ioIntelligenceDefaultModelId,
rooDefaultModelId,
} from "@roo-code/types"
import { vscode } from "@src/utils/vscode"
@ -86,6 +88,7 @@ import {
XAI,
ZAi,
Fireworks,
Featherless,
} from "./providers"
import { MODELS_BY_PROVIDER, PROVIDERS } from "./constants"
@ -124,7 +127,7 @@ const ApiOptions = ({
setErrorMessage,
}: ApiOptionsProps) => {
const { t } = useAppTranslation()
const { organizationAllowList } = useExtensionState()
const { organizationAllowList, cloudIsAuthenticated } = useExtensionState()
const [customHeaders, setCustomHeaders] = useState<[string, string][]>(() => {
const headers = apiConfiguration?.openAiHeaders || {}
@ -326,7 +329,9 @@ const ApiOptions = ({
: internationalZAiDefaultModelId,
},
fireworks: { field: "apiModelId", default: fireworksDefaultModelId },
featherless: { field: "apiModelId", default: featherlessDefaultModelId },
"io-intelligence": { field: "ioIntelligenceModelId", default: ioIntelligenceDefaultModelId },
roo: { field: "apiModelId", default: rooDefaultModelId },
openai: { field: "openAiModelId" },
ollama: { field: "ollamaModelId" },
lmstudio: { field: "lmStudioModelId" },
@ -579,6 +584,29 @@ const ApiOptions = ({
<Fireworks apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProvider === "roo" && (
<div className="flex flex-col gap-3">
{cloudIsAuthenticated ? (
<div className="text-sm text-vscode-descriptionForeground">
{t("settings:providers.roo.authenticatedMessage")}
</div>
) : (
<div className="flex flex-col gap-2">
<VSCodeButton
appearance="primary"
onClick={() => vscode.postMessage({ type: "rooCloudSignIn" })}
className="w-fit">
{t("settings:providers.roo.connectButton")}
</VSCodeButton>
</div>
)}
</div>
)}
{selectedProvider === "featherless" && (
<Featherless apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
)}
{selectedProviderModels.length > 0 && (
<>
<div>

View file

@ -34,7 +34,7 @@ export const ModelDescriptionMarkdown = memo(
return (
<Collapsible open={isExpanded} onOpenChange={setIsExpanded} className="relative">
<div ref={textContainerRef} className={cn({ "line-clamp-3": !isExpanded })}>
<div ref={textContainerRef} className={cn({ "line-clamp-4": !isExpanded })}>
<div ref={textRef}>
<StyledMarkdown key={key}>{content}</StyledMarkdown>
</div>

View file

@ -26,6 +26,18 @@ export const ModelInfoView = ({
const { t } = useAppTranslation()
const infoItems = [
typeof modelInfo?.contextWindow === "number" && modelInfo.contextWindow > 0 && (
<>
<span className="font-medium">{t("settings:modelInfo.contextWindow")}</span>{" "}
{modelInfo.contextWindow?.toLocaleString()} tokens
</>
),
typeof modelInfo?.maxTokens === "number" && modelInfo.maxTokens > 0 && (
<>
<span className="font-medium">{t("settings:modelInfo.maxOutput")}:</span>{" "}
{modelInfo.maxTokens?.toLocaleString()} tokens
</>
),
<ModelInfoSupportsItem
isSupported={modelInfo?.supportsImages ?? false}
supportsLabel={t("settings:modelInfo.supportsImages")}
@ -41,18 +53,6 @@ export const ModelInfoView = ({
supportsLabel={t("settings:modelInfo.supportsPromptCache")}
doesNotSupportLabel={t("settings:modelInfo.noPromptCache")}
/>,
typeof modelInfo?.contextWindow === "number" && modelInfo.contextWindow > 0 && (
<>
<span className="font-medium">{t("settings:modelInfo.contextWindow")}</span>{" "}
{modelInfo.contextWindow?.toLocaleString()} tokens
</>
),
typeof modelInfo?.maxTokens === "number" && modelInfo.maxTokens > 0 && (
<>
<span className="font-medium">{t("settings:modelInfo.maxOutput")}:</span>{" "}
{modelInfo.maxTokens?.toLocaleString()} tokens
</>
),
modelInfo?.inputPrice !== undefined && modelInfo.inputPrice > 0 && (
<>
<span className="font-medium">{t("settings:modelInfo.inputPrice")}:</span>{" "}
@ -119,11 +119,7 @@ const ModelInfoSupportsItem = ({
supportsLabel: string
doesNotSupportLabel: string
}) => (
<div
className={cn(
"flex items-center gap-1 font-medium",
isSupported ? "text-vscode-charts-green" : "text-vscode-errorForeground",
)}>
<div className="flex items-center gap-1 font-medium">
<span className={cn("codicon", isSupported ? "codicon-check" : "codicon-x")} />
{isSupported ? supportsLabel : doesNotSupportLabel}
</div>

View file

@ -46,7 +46,7 @@ const PromptsSettings = ({
} = useExtensionState()
// Use props if provided, otherwise fall back to context
const includeTaskHistoryInEnhance = propsIncludeTaskHistoryInEnhance ?? contextIncludeTaskHistoryInEnhance
const includeTaskHistoryInEnhance = propsIncludeTaskHistoryInEnhance ?? contextIncludeTaskHistoryInEnhance ?? true
const setIncludeTaskHistoryInEnhance = propsSetIncludeTaskHistoryInEnhance ?? contextSetIncludeTaskHistoryInEnhance
const [testPrompt, setTestPrompt] = useState("")
@ -235,7 +235,7 @@ const PromptsSettings = ({
<>
<div>
<VSCodeCheckbox
checked={includeTaskHistoryInEnhance || false}
checked={includeTaskHistoryInEnhance}
onChange={(e: any) => {
const value = e.target.checked
setIncludeTaskHistoryInEnhance(value)

View file

@ -341,7 +341,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
vscode.postMessage({ type: "condensingApiConfigId", text: condensingApiConfigId || "" })
vscode.postMessage({ type: "updateCondensingPrompt", text: customCondensingPrompt || "" })
vscode.postMessage({ type: "updateSupportPrompt", values: customSupportPrompts || {} })
vscode.postMessage({ type: "includeTaskHistoryInEnhance", bool: includeTaskHistoryInEnhance ?? false })
vscode.postMessage({ type: "includeTaskHistoryInEnhance", bool: includeTaskHistoryInEnhance ?? true })
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting })
vscode.postMessage({ type: "profileThresholds", values: profileThresholds })

View file

@ -18,6 +18,8 @@ import {
doubaoModels,
internationalZAiModels,
fireworksModels,
rooModels,
featherlessModels,
} from "@roo-code/types"
export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, ModelInfo>>> = {
@ -38,6 +40,8 @@ export const MODELS_BY_PROVIDER: Partial<Record<ProviderName, Record<string, Mod
sambanova: sambaNovaModels,
zai: internationalZAiModels,
fireworks: fireworksModels,
roo: rooModels,
featherless: featherlessModels,
}
export const PROVIDERS = [
@ -69,5 +73,7 @@ export const PROVIDERS = [
{ value: "sambanova", label: "SambaNova" },
{ value: "zai", label: "Z AI" },
{ value: "fireworks", label: "Fireworks AI" },
{ value: "featherless", label: "Featherless AI" },
{ value: "io-intelligence", label: "IO Intelligence" },
{ value: "roo", label: "Roo Code Cloud" },
].sort((a, b) => a.label.localeCompare(b.label))

View file

@ -0,0 +1,50 @@
import { useCallback } from "react"
import { VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
import type { ProviderSettings } from "@roo-code/types"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { inputEventTransform } from "../transforms"
type FeatherlessProps = {
apiConfiguration: ProviderSettings
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
}
export const Featherless = ({ apiConfiguration, setApiConfigurationField }: FeatherlessProps) => {
const { t } = useAppTranslation()
const handleInputChange = useCallback(
<K extends keyof ProviderSettings, E>(
field: K,
transform: (event: E) => ProviderSettings[K] = inputEventTransform,
) =>
(event: E | Event) => {
setApiConfigurationField(field, transform(event as E))
},
[setApiConfigurationField],
)
return (
<>
<VSCodeTextField
value={apiConfiguration?.featherlessApiKey || ""}
type="password"
onInput={handleInputChange("featherlessApiKey")}
placeholder={t("settings:placeholders.apiKey")}
className="w-full">
<label className="block font-medium mb-1">{t("settings:providers.featherlessApiKey")}</label>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground -mt-2">
{t("settings:providers.apiKeyStorageNotice")}
</div>
{!apiConfiguration?.featherlessApiKey && (
<VSCodeButtonLink href="https://featherless.ai/account/api-keys" appearance="secondary">
{t("settings:providers.getFeatherlessApiKey")}
</VSCodeButtonLink>
)}
</>
)
}

View file

@ -26,3 +26,4 @@ export { XAI } from "./XAI"
export { ZAi } from "./ZAi"
export { LiteLLM } from "./LiteLLM"
export { Fireworks } from "./Fireworks"
export { Featherless } from "./Featherless"

View file

@ -46,8 +46,12 @@ import {
mainlandZAiModels,
fireworksModels,
fireworksDefaultModelId,
featherlessModels,
featherlessDefaultModelId,
ioIntelligenceDefaultModelId,
ioIntelligenceModels,
rooDefaultModelId,
rooModels,
BEDROCK_CLAUDE_SONNET_4_MODEL_ID,
} from "@roo-code/types"
@ -290,12 +294,22 @@ function getSelectedModel({
const info = fireworksModels[id as keyof typeof fireworksModels]
return { id, info }
}
case "featherless": {
const id = apiConfiguration.apiModelId ?? featherlessDefaultModelId
const info = featherlessModels[id as keyof typeof featherlessModels]
return { id, info }
}
case "io-intelligence": {
const id = apiConfiguration.ioIntelligenceModelId ?? ioIntelligenceDefaultModelId
const info =
routerModels["io-intelligence"]?.[id] ?? ioIntelligenceModels[id as keyof typeof ioIntelligenceModels]
return { id, info }
}
case "roo": {
const id = apiConfiguration.apiModelId ?? rooDefaultModelId
const info = rooModels[id as keyof typeof rooModels]
return { id, info }
}
// case "anthropic":
// case "human-relay":
// case "fake-ai":

View file

@ -268,7 +268,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
project: {},
global: {},
})
const [includeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance] = useState(false)
const [includeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance] = useState(true)
const setListApiConfigMeta = useCallback(
(value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),

View file

@ -265,6 +265,12 @@
},
"announcement": {
"title": "🎉 Roo Code {{version}} Llançat",
"stealthModel": {
"feature": "<bold>Model stealth GRATUÏT per temps limitat</bold> - Un model de raonament ultraràpid que destaca en codificació agèntica amb una finestra de context de 262k, disponible a través de Roo Code Cloud.",
"note": "(Nota: els prompts i completacions són registrats pel creador del model i utilitzats per millorar-lo)",
"connectButton": "Connectar a Roo Code Cloud",
"selectModel": "Selecciona <code>roo/sonic</code> del proveïdor Roo Code Cloud a<br/><settingsLink>Configuració</settingsLink> per començar"
},
"description": "Roo Code {{version}} porta noves funcions potents i millores significatives per millorar el vostre flux de treball de desenvolupament.",
"whatsNew": "Novetats",
"feature1": "<bold>Cua de Missatges</bold>: Posa en cua múltiples missatges mentre Roo està treballant, permetent-te continuar planificant el teu flux de treball sense interrupcions.",
@ -349,9 +355,9 @@
"ariaLabel": "Versió {{version}} - Feu clic per veure les notes de llançament"
},
"rooCloudCTA": {
"title": "Roo Code Cloud arribarà aviat!",
"title": "Roo Code Cloud està evolucionant!",
"description": "Executa agents remots al núvol, accedeix a les teves tasques des de qualsevol lloc, col·labora amb altres i molt més.",
"joinWaitlist": "Uneix-te a la llista d'espera per obtenir accés anticipat."
"joinWaitlist": "Registra't per rebre les últimes actualitzacions."
},
"editMessage": {
"placeholder": "Edita el teu missatge..."

View file

@ -269,6 +269,8 @@
"getChutesApiKey": "Obtenir clau API de Chutes",
"fireworksApiKey": "Clau API de Fireworks",
"getFireworksApiKey": "Obtenir clau API de Fireworks",
"featherlessApiKey": "Clau API de Featherless",
"getFeatherlessApiKey": "Obtenir clau API de Featherless",
"ioIntelligenceApiKey": "Clau API d'IO Intelligence",
"ioIntelligenceApiKeyPlaceholder": "Introdueix la teva clau d'API de IO Intelligence",
"getIoIntelligenceApiKey": "Obtenir clau API d'IO Intelligence",
@ -377,6 +379,10 @@
"description": "No es requereix clau API, però l'usuari necessita ajuda per copiar i enganxar informació al xat d'IA web.",
"instructions": "Durant l'ús, apareixerà un diàleg i el missatge actual es copiarà automàticament al porta-retalls. Necessiteu enganxar-lo a les versions web d'IA (com ChatGPT o Claude), després copiar la resposta de l'IA de nou al diàleg i fer clic al botó de confirmació."
},
"roo": {
"authenticatedMessage": "Autenticat de forma segura a través del teu compte de Roo Code Cloud.",
"connectButton": "Connecta amb Roo Code Cloud"
},
"openRouter": {
"providerRouting": {
"title": "Encaminament de Proveïdors d'OpenRouter",

View file

@ -265,6 +265,12 @@
},
"announcement": {
"title": "🎉 Roo Code {{version}} veröffentlicht",
"stealthModel": {
"feature": "<bold>Zeitlich begrenztes KOSTENLOSES Stealth-Modell</bold> - Ein blitzschnelles Reasoning-Modell, das sich bei agentic coding mit einem 262k Kontextfenster auszeichnet, verfügbar über Roo Code Cloud.",
"note": "(Hinweis: Prompts und Vervollständigungen werden vom Modellersteller protokolliert und zur Verbesserung des Modells verwendet)",
"connectButton": "Mit Roo Code Cloud verbinden",
"selectModel": "Wähle <code>roo/sonic</code> vom Roo Code Cloud Provider in<br/><settingsLink>Einstellungen</settingsLink> um zu beginnen"
},
"description": "Roo Code {{version}} bringt mächtige neue Funktionen und bedeutende Verbesserungen, um deinen Entwicklungsworkflow zu verbessern.",
"whatsNew": "Was ist neu",
"feature1": "<bold>Nachrichten-Warteschlange</bold>: Stelle mehrere Nachrichten in die Warteschlange, während Roo arbeitet, damit du deinen Workflow ohne Unterbrechung weiter planen kannst.",
@ -349,9 +355,9 @@
"ariaLabel": "Version {{version}} - Klicken Sie, um die Versionshinweise anzuzeigen"
},
"rooCloudCTA": {
"title": "Roo Code Cloud kommt bald!",
"title": "Roo Code Cloud entwickelt sich weiter!",
"description": "Führe Remote-Agenten in der Cloud aus, greife von überall auf deine Aufgaben zu, arbeite mit anderen zusammen und vieles mehr.",
"joinWaitlist": "Tritt der Warteliste bei, um frühen Zugang zu erhalten."
"joinWaitlist": "Melde dich an, um die neuesten Updates zu erhalten."
},
"command": {
"triggerDescription": "Starte den {{name}} Befehl"

View file

@ -271,6 +271,8 @@
"getChutesApiKey": "Chutes API-Schlüssel erhalten",
"fireworksApiKey": "Fireworks API-Schlüssel",
"getFireworksApiKey": "Fireworks API-Schlüssel erhalten",
"featherlessApiKey": "Featherless API-Schlüssel",
"getFeatherlessApiKey": "Featherless API-Schlüssel erhalten",
"ioIntelligenceApiKey": "IO Intelligence API-Schlüssel",
"ioIntelligenceApiKeyPlaceholder": "Gib deinen IO Intelligence API-Schlüssel ein",
"getIoIntelligenceApiKey": "IO Intelligence API-Schlüssel erhalten",
@ -377,6 +379,10 @@
"description": "Es ist kein API-Schlüssel erforderlich, aber der Benutzer muss beim Kopieren und Einfügen der Informationen in den Web-Chat-KI helfen.",
"instructions": "Während der Verwendung wird ein Dialogfeld angezeigt und die aktuelle Nachricht wird automatisch in die Zwischenablage kopiert. Du musst diese in Web-Versionen von KI (wie ChatGPT oder Claude) einfügen, dann die Antwort der KI zurück in das Dialogfeld kopieren und auf die Bestätigungsschaltfläche klicken."
},
"roo": {
"authenticatedMessage": "Sicher authentifiziert über dein Roo Code Cloud-Konto.",
"connectButton": "Mit Roo Code Cloud verbinden"
},
"openRouter": {
"providerRouting": {
"title": "OpenRouter Anbieter-Routing",

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