diff --git a/.env.sample b/.env.sample index d89ef72792..aebe5cca44 100644 --- a/.env.sample +++ b/.env.sample @@ -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 diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 965566a319..c659eb80eb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -25,6 +25,7 @@ body: - AWS Bedrock - Chutes AI - DeepSeek + - Featherless AI - Fireworks AI - Glama - Google Gemini diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c97f6fcb2..8f4cccac46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/packages/ipc/README.md b/packages/ipc/README.md new file mode 100644 index 0000000000..7642af4958 --- /dev/null +++ b/packages/ipc/README.md @@ -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}` diff --git a/packages/types/npm/package.metadata.json b/packages/types/npm/package.metadata.json index e33d5530af..70ab13bb4e 100644 --- a/packages/types/npm/package.metadata.json +++ b/packages/types/npm/package.metadata.json @@ -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", diff --git a/packages/types/src/__tests__/ipc.test.ts b/packages/types/src/__tests__/ipc.test.ts new file mode 100644 index 0000000000..a2b4429356 --- /dev/null +++ b/packages/types/src/__tests__/ipc.test.ts @@ -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) + }) + }) +}) diff --git a/packages/types/src/events.ts b/packages/types/src/events.ts index 42c389ab60..2b6b810c81 100644 --- a/packages/types/src/events.ts +++ b/packages/types/src/events.ts @@ -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], diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index c071726d8a..dd72d72fe9 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -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 diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index c65d070086..b151067d1d 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -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" diff --git a/packages/types/src/ipc.ts b/packages/types/src/ipc.ts index 22cba1dea8..ace39c3f2b 100644 --- a/packages/types/src/ipc.ts +++ b/packages/types/src/ipc.ts @@ -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 diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index 7197ab29a1..5037370f24 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -44,24 +44,61 @@ export const clineAskSchema = z.enum(clineAsks) export type ClineAsk = z.infer +// 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) } /** diff --git a/packages/types/src/model.ts b/packages/types/src/model.ts index 90b61ad879..969a4caa96 100644 --- a/packages/types/src/model.ts +++ b/packages/types/src/model.ts @@ -10,6 +10,14 @@ export const reasoningEffortsSchema = z.enum(reasoningEfforts) export type ReasoningEffort = z.infer +/** + * ReasoningEffortWithMinimal + */ + +export const reasoningEffortWithMinimalSchema = z.union([reasoningEffortsSchema, z.literal("minimal")]) + +export type ReasoningEffortWithMinimal = z.infer + /** * Verbosity */ diff --git a/packages/types/src/provider-settings.ts b/packages/types/src/provider-settings.ts index fef7d811a4..c13319a956 100644 --- a/packages/types/src/provider-settings.ts +++ b/packages/types/src/provider-settings.ts @@ -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 +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, + { 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) diff --git a/packages/types/src/providers/bedrock.ts b/packages/types/src/providers/bedrock.ts index d7319c132e..67215e7796 100644 --- a/packages/types/src/providers/bedrock.ts +++ b/packages/types/src/providers/bedrock.ts @@ -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" diff --git a/packages/types/src/providers/featherless.ts b/packages/types/src/providers/featherless.ts new file mode 100644 index 0000000000..d24f1fd882 --- /dev/null +++ b/packages/types/src/providers/featherless.ts @@ -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 + +export const featherlessDefaultModelId: FeatherlessModelId = "deepseek-ai/DeepSeek-R1-0528" diff --git a/packages/types/src/providers/index.ts b/packages/types/src/providers/index.ts index b7f1cd334e..8ca9c2c9b2 100644 --- a/packages/types/src/providers/index.ts +++ b/packages/types/src/providers/index.ts @@ -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" diff --git a/packages/types/src/providers/openai.ts b/packages/types/src/providers/openai.ts index ff79824984..6409e67586 100644 --- a/packages/types/src/providers/openai.ts +++ b/packages/types/src/providers/openai.ts @@ -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, diff --git a/packages/types/src/providers/roo.ts b/packages/types/src/providers/roo.ts new file mode 100644 index 0000000000..c95e500f54 --- /dev/null +++ b/packages/types/src/providers/roo.ts @@ -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 diff --git a/packages/types/src/providers/vscode-llm.ts b/packages/types/src/providers/vscode-llm.ts index bf38cb814b..efe0691913 100644 --- a/packages/types/src/providers/vscode-llm.ts +++ b/packages/types/src/providers/vscode-llm.ts @@ -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, diff --git a/packages/types/src/single-file-read-models.ts b/packages/types/src/single-file-read-models.ts new file mode 100644 index 0000000000..4e6f4e1f57 --- /dev/null +++ b/packages/types/src/single-file-read-models.ts @@ -0,0 +1,32 @@ +/** + * Configuration for models that should use simplified single-file read_file tool + * These models will use the simpler ... 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(["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 +} diff --git a/packages/types/src/task.ts b/packages/types/src/task.ts index 9dc0f1a574..f211227c02 100644 --- a/packages/types/src/task.ts +++ b/packages/types/src/task.ts @@ -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 cancelTask(): Promise clearTask(): Promise + resumeTask(taskId: string): void getState(): Promise postStateToWebview(): Promise @@ -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 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(event: K, listener: (...args: TaskEvents[K]) => void | Promise): this off(event: K, listener: (...args: TaskEvents[K]) => void | Promise): 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 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0bdcf9ab8..ff063ecb0d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src/api/index.ts b/src/api/index.ts index c29c230b06..48a0a89ec5 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -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 @@ -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) diff --git a/src/api/providers/__tests__/featherless.spec.ts b/src/api/providers/__tests__/featherless.spec.ts new file mode 100644 index 0000000000..b0b4c01b86 --- /dev/null +++ b/src/api/providers/__tests__/featherless.spec.ts @@ -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: "Thinking..." }, + index: 0, + }, + ], + usage: null, + } + yield { + choices: [ + { + delta: { content: "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) + }) +}) diff --git a/src/api/providers/__tests__/native-ollama.spec.ts b/src/api/providers/__tests__/native-ollama.spec.ts new file mode 100644 index 0000000000..f8792937db --- /dev/null +++ b/src/api/providers/__tests__/native-ollama.spec.ts @@ -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: "Let me think" } } + yield { message: { content: " about this" } } + 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() + }) + }) +}) diff --git a/src/api/providers/__tests__/roo.spec.ts b/src/api/providers/__tests__/roo.spec.ts new file mode 100644 index 0000000000..b16a6e7ac1 --- /dev/null +++ b/src/api/providers/__tests__/roo.spec.ts @@ -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 + + 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") + }) + }) +}) diff --git a/src/api/providers/base-openai-compatible-provider.ts b/src/api/providers/base-openai-compatible-provider.ts index f196b5f309..3c824f2651 100644 --- a/src/api/providers/base-openai-compatible-provider.ts +++ b/src/api/providers/base-openai-compatible-provider.ts @@ -62,11 +62,11 @@ export abstract class BaseOpenAiCompatibleProvider }) } - 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 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 diff --git a/src/api/providers/featherless.ts b/src/api/providers/featherless.ts new file mode 100644 index 0000000000..2a985e2a87 --- /dev/null +++ b/src/api/providers/featherless.ts @@ -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 { + 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, + }, + } + } +} diff --git a/src/api/providers/fetchers/__tests__/lmstudio.test.ts b/src/api/providers/fetchers/__tests__/lmstudio.test.ts index ff9a109e50..8e7e36c73f 100644 --- a/src/api/providers/fetchers/__tests__/lmstudio.test.ts +++ b/src/api/providers/fetchers/__tests__/lmstudio.test.ts @@ -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" diff --git a/src/api/providers/fetchers/lmstudio.ts b/src/api/providers/fetchers/lmstudio.ts index 976822c67d..1e2e016df2 100644 --- a/src/api/providers/fetchers/lmstudio.ts +++ b/src/api/providers/fetchers/lmstudio.ts @@ -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 + // 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) } diff --git a/src/api/providers/index.ts b/src/api/providers/index.ts index 736da82d51..d256fbbe55 100644 --- a/src/api/providers/index.ts +++ b/src/api/providers/index.ts @@ -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" diff --git a/src/api/providers/native-ollama.ts b/src/api/providers/native-ollama.ts new file mode 100644 index 0000000000..8ab4ebe2e1 --- /dev/null +++ b/src/api/providers/native-ollama.ts @@ -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 = {} + + 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 { + 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 + } + } +} diff --git a/src/api/providers/roo.ts b/src/api/providers/roo.ts new file mode 100644 index 0000000000..d986d6cd10 --- /dev/null +++ b/src/api/providers/roo.ts @@ -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 { + 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, + }, + } + } +} diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index acdc7f5412..a8b90728b1 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -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) diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap index 632273dea0..cfec773b44 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap @@ -270,29 +270,22 @@ Examples: ## 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 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: suggestion text - - 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 tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.) + Usage: Your question here - -Your suggested answer here - - -Implement the solution - +First suggestion +Action with mode switch -Example: Requesting to ask the user for the path to the frontend-config.json file +Example: What is the path to the frontend-config.json file? @@ -302,16 +295,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil -Example: Asking a question with mode switching options - -How would you like to proceed with this task? - -Start implementing the solution -Plan the architecture first -Continue with more details - - - ## 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 tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap index 09b6b04348..0289fc2200 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap @@ -167,29 +167,22 @@ Examples: ## 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 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: suggestion text - - 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 tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.) + Usage: Your question here - -Your suggested answer here - - -Implement the solution - +First suggestion +Action with mode switch -Example: Requesting to ask the user for the path to the frontend-config.json file +Example: What is the path to the frontend-config.json file? @@ -199,16 +192,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil -Example: Asking a question with mode switching options - -How would you like to proceed with this task? - -Start implementing the solution -Plan the architecture first -Continue with more details - - - ## 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 tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap index b1fdcc2e32..f16c11a97f 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap @@ -269,29 +269,22 @@ Examples: ## 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 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: suggestion text - - 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 tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.) + Usage: Your question here - -Your suggested answer here - - -Implement the solution - +First suggestion +Action with mode switch -Example: Requesting to ask the user for the path to the frontend-config.json file +Example: What is the path to the frontend-config.json file? @@ -301,16 +294,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil -Example: Asking a question with mode switching options - -How would you like to proceed with this task? - -Start implementing the solution -Plan the architecture first -Continue with more details - - - ## 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 tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap index 7ca32b80a1..168dd87d59 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap @@ -319,29 +319,22 @@ Example: Requesting to access an 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 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: suggestion text - - 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 tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.) + Usage: Your question here - -Your suggested answer here - - -Implement the solution - +First suggestion +Action with mode switch -Example: Requesting to ask the user for the path to the frontend-config.json file +Example: What is the path to the frontend-config.json file? @@ -351,16 +344,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil -Example: Asking a question with mode switching options - -How would you like to proceed with this task? - -Start implementing the solution -Plan the architecture first -Continue with more details - - - ## 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 tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap index 7dce6219f3..bafff056b5 100644 --- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap +++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap @@ -275,29 +275,22 @@ Examples: ## 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 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: suggestion text - - 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 tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.) + Usage: Your question here - -Your suggested answer here - - -Implement the solution - +First suggestion +Action with mode switch -Example: Requesting to ask the user for the path to the frontend-config.json file +Example: What is the path to the frontend-config.json file? @@ -307,16 +300,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil -Example: Asking a question with mode switching options - -How would you like to proceed with this task? - -Start implementing the solution -Plan the architecture first -Continue with more details - - - ## 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 tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap index 632273dea0..cfec773b44 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap @@ -270,29 +270,22 @@ Examples: ## 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 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: suggestion text - - 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 tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.) + Usage: Your question here - -Your suggested answer here - - -Implement the solution - +First suggestion +Action with mode switch -Example: Requesting to ask the user for the path to the frontend-config.json file +Example: What is the path to the frontend-config.json file? @@ -302,16 +295,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil -Example: Asking a question with mode switching options - -How would you like to proceed with this task? - -Start implementing the solution -Plan the architecture first -Continue with more details - - - ## 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 tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap index 419049609e..fb1fb5c77e 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap @@ -323,29 +323,22 @@ Example: Requesting to click on the element at coordinates 450,300 ## 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 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: suggestion text - - 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 tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.) + Usage: Your question here - -Your suggested answer here - - -Implement the solution - +First suggestion +Action with mode switch -Example: Requesting to ask the user for the path to the frontend-config.json file +Example: What is the path to the frontend-config.json file? @@ -355,16 +348,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil -Example: Asking a question with mode switching options - -How would you like to proceed with this task? - -Start implementing the solution -Plan the architecture first -Continue with more details - - - ## 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 tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap index 632273dea0..cfec773b44 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap @@ -270,29 +270,22 @@ Examples: ## 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 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: suggestion text - - 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 tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.) + Usage: Your question here - -Your suggested answer here - - -Implement the solution - +First suggestion +Action with mode switch -Example: Requesting to ask the user for the path to the frontend-config.json file +Example: What is the path to the frontend-config.json file? @@ -302,16 +295,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil -Example: Asking a question with mode switching options - -How would you like to proceed with this task? - -Start implementing the solution -Plan the architecture first -Continue with more details - - - ## 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 tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap index 7a60f1d403..7c0108ff75 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap @@ -358,29 +358,22 @@ Examples: ## 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 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: suggestion text - - 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 tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.) + Usage: Your question here - -Your suggested answer here - - -Implement the solution - +First suggestion +Action with mode switch -Example: Requesting to ask the user for the path to the frontend-config.json file +Example: What is the path to the frontend-config.json file? @@ -390,16 +383,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil -Example: Asking a question with mode switching options - -How would you like to proceed with this task? - -Start implementing the solution -Plan the architecture first -Continue with more details - - - ## 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 tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap index 632273dea0..cfec773b44 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap @@ -270,29 +270,22 @@ Examples: ## 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 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: suggestion text - - 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 tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.) + Usage: Your question here - -Your suggested answer here - - -Implement the solution - +First suggestion +Action with mode switch -Example: Requesting to ask the user for the path to the frontend-config.json file +Example: What is the path to the frontend-config.json file? @@ -302,16 +295,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil -Example: Asking a question with mode switching options - -How would you like to proceed with this task? - -Start implementing the solution -Plan the architecture first -Continue with more details - - - ## 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 tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap index 191816f180..bf764fdf83 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap @@ -323,29 +323,22 @@ Example: Requesting to click on the element at coordinates 450,300 ## 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 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: suggestion text - - 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 tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.) + Usage: Your question here - -Your suggested answer here - - -Implement the solution - +First suggestion +Action with mode switch -Example: Requesting to ask the user for the path to the frontend-config.json file +Example: What is the path to the frontend-config.json file? @@ -355,16 +348,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil -Example: Asking a question with mode switching options - -How would you like to proceed with this task? - -Start implementing the solution -Plan the architecture first -Continue with more details - - - ## 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 tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap index 7ca32b80a1..168dd87d59 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap @@ -319,29 +319,22 @@ Example: Requesting to access an 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 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: suggestion text - - 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 tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.) + Usage: Your question here - -Your suggested answer here - - -Implement the solution - +First suggestion +Action with mode switch -Example: Requesting to ask the user for the path to the frontend-config.json file +Example: What is the path to the frontend-config.json file? @@ -351,16 +344,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil -Example: Asking a question with mode switching options - -How would you like to proceed with this task? - -Start implementing the solution -Plan the architecture first -Continue with more details - - - ## 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 tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap index 632273dea0..cfec773b44 100644 --- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap +++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap @@ -270,29 +270,22 @@ Examples: ## 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 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: suggestion text - - 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 tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.) + Usage: Your question here - -Your suggested answer here - - -Implement the solution - +First suggestion +Action with mode switch -Example: Requesting to ask the user for the path to the frontend-config.json file +Example: What is the path to the frontend-config.json file? @@ -302,16 +295,6 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil -Example: Asking a question with mode switching options - -How would you like to proceed with this task? - -Start implementing the solution -Plan the architecture first -Continue with more details - - - ## 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 tags if you've confirmed from the user that any previous tool uses were successful. If not, then DO NOT use this tool. diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts index 4ed1185da7..3cc327c815 100644 --- a/src/core/prompts/system.ts +++ b/src/core/prompts/system.ts @@ -61,6 +61,7 @@ async function generatePrompt( partialReadsEnabled?: boolean, settings?: SystemPromptSettings, todoList?: TodoItem[], + modelId?: string, ): Promise { 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 => { if (!context) { throw new Error("Extension context is required for generating system prompt") @@ -221,5 +224,6 @@ ${customInstructions}` partialReadsEnabled, settings, todoList, + modelId, ) } diff --git a/src/core/prompts/tools/ask-followup-question.ts b/src/core/prompts/tools/ask-followup-question.ts index c69e5a697f..c40684b8bc 100644 --- a/src/core/prompts/tools/ask-followup-question.ts +++ b/src/core/prompts/tools/ask-followup-question.ts @@ -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 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: suggestion text - - 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 tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.) + Usage: Your question here - -Your suggested answer here - - -Implement the solution - +First suggestion +Action with mode switch -Example: Requesting to ask the user for the path to the frontend-config.json file +Example: What is the path to the frontend-config.json file? @@ -30,15 +23,5 @@ Example: Requesting to ask the user for the path to the frontend-config.json fil ./config/frontend-config.json ./frontend-config.json - - -Example: Asking a question with mode switching options - -How would you like to proceed with this task? - -Start implementing the solution -Plan the architecture first -Continue with more details - ` } diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts index d455abb8d7..3eb112d270 100644 --- a/src/core/prompts/tools/index.ts +++ b/src/core/prompts/tools/index.ts @@ -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 | 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, 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, diff --git a/src/core/prompts/tools/simple-read-file.ts b/src/core/prompts/tools/simple-read-file.ts new file mode 100644 index 0000000000..28f4f1129e --- /dev/null +++ b/src/core/prompts/tools/simple-read-file.ts @@ -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: file/path.ext + */ +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: + +path/to/file + + +Examples: + +1. Reading a TypeScript file: + +src/app.ts + + +2. Reading a configuration file: + +config.json + + +3. Reading a markdown file: + +README.md +` +} diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index e882450c35..d1fa000026 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -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 implements TaskLike { providerRef: WeakRef 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 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 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 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 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 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 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 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 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 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 implements TaskLike { todoListEnabled: apiConfiguration?.todoListEnabled ?? true, useAgentRules: vscode.workspace.getConfiguration("roo-cline").get("useAgentRules") ?? true, }, + undefined, // todoList + this.api.getModel().id, ) })() } @@ -2545,4 +2615,24 @@ export class Task extends EventEmitter 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 + } } diff --git a/src/core/task/__tests__/Task.spec.ts b/src/core/task/__tests__/Task.spec.ts index 39df433814..01469ddbf5 100644 --- a/src/core/task/__tests__/Task.spec.ts +++ b/src/core/task/__tests__/Task.spec.ts @@ -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() + }) + }) }) diff --git a/src/core/tools/__tests__/executeCommand.spec.ts b/src/core/tools/__tests__/executeCommand.spec.ts index 68dec5c456..2e973a24cb 100644 --- a/src/core/tools/__tests__/executeCommand.spec.ts +++ b/src/core/tools/__tests__/executeCommand.spec.ts @@ -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") }) }) diff --git a/src/core/tools/executeCommandTool.ts b/src/core/tools/executeCommandTool.ts index c346526a2e..2c7ce0d023 100644 --- a/src/core/tools/executeCommandTool.ts +++ b/src/core/tools/executeCommandTool.ts @@ -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) diff --git a/src/core/tools/simpleReadFileTool.ts b/src/core/tools/simpleReadFileTool.ts new file mode 100644 index 0000000000..ee6656c5c8 --- /dev/null +++ b/src/core/tools/simpleReadFileTool.ts @@ -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: file/path.ext + * + * 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(`${errorMsg}`) + 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(`${relPath}${errorMsg}`) + 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${relPath}Denied by user`) + 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( + `${relPath}\n${validationResult.notice}\n`, + ) + 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( + `${relPath}\n${imageResult.notice}\n`, + 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( + `${relPath}Error reading image file: ${errorMsg}`, + ) + 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( + `${relPath}\nBinary file - content not displayed\n`, + ) + return + } + } + + // Handle definitions-only mode + if (maxReadFileLine === 0) { + try { + const defResult = await parseSourceCodeDefinitionsForFile(fullPath, cline.rooIgnoreController) + if (defResult) { + let xmlInfo = `Showing only definitions. Use standard read_file if you need to read actual content\n` + pushToolResult( + `${relPath}\n${defResult}\n${xmlInfo}`, + ) + } + } 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 = `\n${content}\n` + + try { + const defResult = await parseSourceCodeDefinitionsForFile(fullPath, cline.rooIgnoreController) + if (defResult) { + xmlInfo += `${defResult}\n` + } + xmlInfo += `Showing only ${maxReadFileLine} of ${totalLines} total lines. File is too large for complete display\n` + pushToolResult(`${relPath}\n${xmlInfo}`) + } 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 ? `\n${content}\n` : `` + + if (totalLines === 0) { + xmlInfo += `File is empty\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${relPath}\n${xmlInfo}`) + } else { + pushToolResult(`${relPath}\n${xmlInfo}`) + } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error) + pushToolResult(`${relPath}Error reading file: ${errorMsg}`) + 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]` + } +} diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 6693d570d6..3c731fac04 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -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, } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index d16ed06132..4dd0fee75e 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -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": diff --git a/src/extension/api.ts b/src/extension/api.ts index 7378f2ca5c..315ad9304a 100644 --- a/src/extension/api.ts +++ b/src/extension/api.ts @@ -78,6 +78,17 @@ export class API extends EventEmitter 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 } }) } diff --git a/src/i18n/locales/ca/common.json b/src/i18n/locales/ca/common.json index d4fddfebf3..6235593f7e 100644 --- a/src/i18n/locales/ca/common.json +++ b/src/i18n/locales/ca/common.json @@ -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": { diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index af69ed7cfe..6819b27d73 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -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": { diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 05d039a495..696ecb44d4 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -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": { diff --git a/src/i18n/locales/es/common.json b/src/i18n/locales/es/common.json index 28e2fc3812..c1b399b84f 100644 --- a/src/i18n/locales/es/common.json +++ b/src/i18n/locales/es/common.json @@ -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": { diff --git a/src/i18n/locales/fr/common.json b/src/i18n/locales/fr/common.json index c3264e7ba3..682e12e224 100644 --- a/src/i18n/locales/fr/common.json +++ b/src/i18n/locales/fr/common.json @@ -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": { diff --git a/src/i18n/locales/hi/common.json b/src/i18n/locales/hi/common.json index c68d002e1d..05e0a622cc 100644 --- a/src/i18n/locales/hi/common.json +++ b/src/i18n/locales/hi/common.json @@ -100,6 +100,9 @@ "genericError": "Cerebras API त्रुटि ({{status}}): {{message}}", "noResponseBody": "Cerebras API त्रुटि: कोई प्रतिक्रिया मुख्य भाग नहीं", "completionError": "Cerebras पूर्णता त्रुटि: {{error}}" + }, + "roo": { + "authenticationRequired": "Roo प्रदाता को क्लाउड प्रमाणीकरण की आवश्यकता है। कृपया Roo Code Cloud में साइन इन करें।" } }, "warnings": { diff --git a/src/i18n/locales/id/common.json b/src/i18n/locales/id/common.json index 4045b7e8bb..1595b795cf 100644 --- a/src/i18n/locales/id/common.json +++ b/src/i18n/locales/id/common.json @@ -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": { diff --git a/src/i18n/locales/it/common.json b/src/i18n/locales/it/common.json index a059c48a9b..73f4d47788 100644 --- a/src/i18n/locales/it/common.json +++ b/src/i18n/locales/it/common.json @@ -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": { diff --git a/src/i18n/locales/ja/common.json b/src/i18n/locales/ja/common.json index f9eceb5979..cb55c7bf0b 100644 --- a/src/i18n/locales/ja/common.json +++ b/src/i18n/locales/ja/common.json @@ -100,6 +100,9 @@ "genericError": "Cerebras APIエラー ({{status}}): {{message}}", "noResponseBody": "Cerebras APIエラー: レスポンスボディなし", "completionError": "Cerebras完了エラー: {{error}}" + }, + "roo": { + "authenticationRequired": "Rooプロバイダーはクラウド認証が必要です。Roo Code Cloudにサインインしてください。" } }, "warnings": { diff --git a/src/i18n/locales/ko/common.json b/src/i18n/locales/ko/common.json index 4292677e30..9bb61b6563 100644 --- a/src/i18n/locales/ko/common.json +++ b/src/i18n/locales/ko/common.json @@ -100,6 +100,9 @@ "genericError": "Cerebras API 오류 ({{status}}): {{message}}", "noResponseBody": "Cerebras API 오류: 응답 본문 없음", "completionError": "Cerebras 완료 오류: {{error}}" + }, + "roo": { + "authenticationRequired": "Roo 제공업체는 클라우드 인증이 필요합니다. Roo Code Cloud에 로그인하세요." } }, "warnings": { diff --git a/src/i18n/locales/nl/common.json b/src/i18n/locales/nl/common.json index 2133e7003b..fb2fcec9f9 100644 --- a/src/i18n/locales/nl/common.json +++ b/src/i18n/locales/nl/common.json @@ -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": { diff --git a/src/i18n/locales/pl/common.json b/src/i18n/locales/pl/common.json index 1c63702911..2a6fee3e23 100644 --- a/src/i18n/locales/pl/common.json +++ b/src/i18n/locales/pl/common.json @@ -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": { diff --git a/src/i18n/locales/pt-BR/common.json b/src/i18n/locales/pt-BR/common.json index 6aeb5f6ee7..83d960ad2d 100644 --- a/src/i18n/locales/pt-BR/common.json +++ b/src/i18n/locales/pt-BR/common.json @@ -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": { diff --git a/src/i18n/locales/ru/common.json b/src/i18n/locales/ru/common.json index 0d7b800d8b..9c37cfe3ed 100644 --- a/src/i18n/locales/ru/common.json +++ b/src/i18n/locales/ru/common.json @@ -100,6 +100,9 @@ "genericError": "Ошибка Cerebras API ({{status}}): {{message}}", "noResponseBody": "Ошибка Cerebras API: Нет тела ответа", "completionError": "Ошибка завершения Cerebras: {{error}}" + }, + "roo": { + "authenticationRequired": "Провайдер Roo требует облачной аутентификации. Войдите в Roo Code Cloud." } }, "warnings": { diff --git a/src/i18n/locales/tr/common.json b/src/i18n/locales/tr/common.json index 49d0fba113..d99008755e 100644 --- a/src/i18n/locales/tr/common.json +++ b/src/i18n/locales/tr/common.json @@ -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": { diff --git a/src/i18n/locales/vi/common.json b/src/i18n/locales/vi/common.json index 3f8947e620..d29525cc03 100644 --- a/src/i18n/locales/vi/common.json +++ b/src/i18n/locales/vi/common.json @@ -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": { diff --git a/src/i18n/locales/zh-CN/common.json b/src/i18n/locales/zh-CN/common.json index 808d534572..fc0386c95d 100644 --- a/src/i18n/locales/zh-CN/common.json +++ b/src/i18n/locales/zh-CN/common.json @@ -105,6 +105,9 @@ "genericError": "Cerebras API 错误 ({{status}}):{{message}}", "noResponseBody": "Cerebras API 错误:无响应主体", "completionError": "Cerebras 完成错误:{{error}}" + }, + "roo": { + "authenticationRequired": "Roo 提供商需要云认证。请登录 Roo Code Cloud。" } }, "warnings": { diff --git a/src/i18n/locales/zh-TW/common.json b/src/i18n/locales/zh-TW/common.json index 81e098fbcf..753463b9f5 100644 --- a/src/i18n/locales/zh-TW/common.json +++ b/src/i18n/locales/zh-TW/common.json @@ -100,6 +100,9 @@ "noResponseBody": "Cerebras API 錯誤:無回應主體", "completionError": "Cerebras 完成錯誤:{{error}}" }, + "roo": { + "authenticationRequired": "Roo 提供者需要雲端認證。請登入 Roo Code Cloud。" + }, "mode_import_failed": "匯入模式失敗:{{error}}" }, "warnings": { diff --git a/src/integrations/terminal/TerminalRegistry.ts b/src/integrations/terminal/TerminalRegistry.ts index af334611c3..6e0531bebe 100644 --- a/src/integrations/terminal/TerminalRegistry.ts +++ b/src/integrations/terminal/TerminalRegistry.ts @@ -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 { @@ -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) diff --git a/src/package.json b/src/package.json index 296e54abc7..c655e19ce6 100644 --- a/src/package.json +++ b/src/package.json @@ -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", diff --git a/src/shared/ProfileValidator.ts b/src/shared/ProfileValidator.ts index afdf1b5232..51eed227d3 100644 --- a/src/shared/ProfileValidator.ts +++ b/src/shared/ProfileValidator.ts @@ -70,6 +70,7 @@ export class ProfileValidator { case "sambanova": case "chutes": case "fireworks": + case "featherless": return profile.apiModelId case "litellm": return profile.litellmModelId diff --git a/src/shared/__tests__/ProfileValidator.spec.ts b/src/shared/__tests__/ProfileValidator.spec.ts index 0129f8fc20..4396e8268a 100644 --- a/src/shared/__tests__/ProfileValidator.spec.ts +++ b/src/shared/__tests__/ProfileValidator.spec.ts @@ -195,6 +195,7 @@ describe("ProfileValidator", () => { "chutes", "sambanova", "fireworks", + "featherless", ] apiModelProviders.forEach((provider) => { diff --git a/src/shared/__tests__/api.spec.ts b/src/shared/__tests__/api.spec.ts index a56fea8e14..7c25fe4197 100644 --- a/src/shared/__tests__/api.spec.ts +++ b/src/shared/__tests__/api.spec.ts @@ -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, diff --git a/src/shared/api.ts b/src/shared/api.ts index f1bf7dbaea..274779fc16 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -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 diff --git a/src/shared/checkExistApiConfig.ts b/src/shared/checkExistApiConfig.ts index eca2dd4fe0..8acb88ed3f 100644 --- a/src/shared/checkExistApiConfig.ts +++ b/src/shared/checkExistApiConfig.ts @@ -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 } diff --git a/webview-ui/src/components/chat/Announcement.tsx b/webview-ui/src/components/chat/Announcement.tsx index 65b78c3cd6..90ad7dffb1 100644 --- a/webview-ui/src/components/chat/Announcement.tsx +++ b/webview-ui/src/components/chat/Announcement.tsx @@ -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 ( { {t("chat:announcement.title", { version: Package.version })} - - {t("chat:announcement.description", { version: Package.version })} -
-

{t("chat:announcement.whatsNew")}

  • •{" "} , - code: , - settingsLink: ( - { - e.preventDefault() - setOpen(false) - hideAnnouncement() - window.postMessage( - { - type: "action", - action: "settingsButtonClicked", - values: { section: "codebaseIndexing" }, - }, - "*", - ) - }} - /> - ), - }} - /> -
  • -
  • - •{" "} - , - code: , - }} - /> -
  • -
  • - •{" "} - , - code: , }} />
- , redditLink: }} - /> + +

{t("chat:announcement.stealthModel.note")}

+ +
+ {!cloudIsAuthenticated ? ( + + ) : ( +
+ , + settingsLink: ( + { + e.preventDefault() + setOpen(false) + hideAnnouncement() + window.postMessage( + { + type: "action", + action: "settingsButtonClicked", + values: { section: "provider" }, + }, + "*", + ) + }} + /> + ), + }} + /> +
+ )} +
) } -const DiscordLink = () => ( - { - e.preventDefault() - window.postMessage( - { type: "action", action: "openExternal", data: { url: "https://discord.gg/roocode" } }, - "*", - ) - }}> - Discord - -) - -const RedditLink = () => ( - { - e.preventDefault() - window.postMessage( - { type: "action", action: "openExternal", data: { url: "https://reddit.com/r/RooCode" } }, - "*", - ) - }}> - Reddit - -) - export default memo(Announcement) diff --git a/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx b/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx index 42c2d6ff50..c7da43032b 100644 --- a/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/Announcement.spec.tsx @@ -11,17 +11,27 @@ vi.mock("@src/components/ui", () => ({ DialogDescription: ({ children }: { children: React.ReactNode }) =>
{children}
, DialogHeader: ({ children }: { children: React.ReactNode }) =>
{children}
, DialogTitle: ({ children }: { children: React.ReactNode }) =>
{children}
, + DialogFooter: ({ children }: { children: React.ReactNode }) =>
{children}
, + Button: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => ( + + ), })) -// 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 }) => ( + {children} + ), +})) + +// 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() - // 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() }) }) diff --git a/webview-ui/src/components/settings/ApiOptions.tsx b/webview-ui/src/components/settings/ApiOptions.tsx index b51b171354..787a95b166 100644 --- a/webview-ui/src/components/settings/ApiOptions.tsx +++ b/webview-ui/src/components/settings/ApiOptions.tsx @@ -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 = ({ )} + {selectedProvider === "roo" && ( +
+ {cloudIsAuthenticated ? ( +
+ {t("settings:providers.roo.authenticatedMessage")} +
+ ) : ( +
+ vscode.postMessage({ type: "rooCloudSignIn" })} + className="w-fit"> + {t("settings:providers.roo.connectButton")} + +
+ )} +
+ )} + + {selectedProvider === "featherless" && ( + + )} + {selectedProviderModels.length > 0 && ( <>
diff --git a/webview-ui/src/components/settings/ModelDescriptionMarkdown.tsx b/webview-ui/src/components/settings/ModelDescriptionMarkdown.tsx index d6aac8ba74..b04ab1163e 100644 --- a/webview-ui/src/components/settings/ModelDescriptionMarkdown.tsx +++ b/webview-ui/src/components/settings/ModelDescriptionMarkdown.tsx @@ -34,7 +34,7 @@ export const ModelDescriptionMarkdown = memo( return ( -
+
{content}
diff --git a/webview-ui/src/components/settings/ModelInfoView.tsx b/webview-ui/src/components/settings/ModelInfoView.tsx index 2ba732effa..5091fb1a68 100644 --- a/webview-ui/src/components/settings/ModelInfoView.tsx +++ b/webview-ui/src/components/settings/ModelInfoView.tsx @@ -26,6 +26,18 @@ export const ModelInfoView = ({ const { t } = useAppTranslation() const infoItems = [ + typeof modelInfo?.contextWindow === "number" && modelInfo.contextWindow > 0 && ( + <> + {t("settings:modelInfo.contextWindow")}{" "} + {modelInfo.contextWindow?.toLocaleString()} tokens + + ), + typeof modelInfo?.maxTokens === "number" && modelInfo.maxTokens > 0 && ( + <> + {t("settings:modelInfo.maxOutput")}:{" "} + {modelInfo.maxTokens?.toLocaleString()} tokens + + ), , - typeof modelInfo?.contextWindow === "number" && modelInfo.contextWindow > 0 && ( - <> - {t("settings:modelInfo.contextWindow")}{" "} - {modelInfo.contextWindow?.toLocaleString()} tokens - - ), - typeof modelInfo?.maxTokens === "number" && modelInfo.maxTokens > 0 && ( - <> - {t("settings:modelInfo.maxOutput")}:{" "} - {modelInfo.maxTokens?.toLocaleString()} tokens - - ), modelInfo?.inputPrice !== undefined && modelInfo.inputPrice > 0 && ( <> {t("settings:modelInfo.inputPrice")}:{" "} @@ -119,11 +119,7 @@ const ModelInfoSupportsItem = ({ supportsLabel: string doesNotSupportLabel: string }) => ( -
+
{isSupported ? supportsLabel : doesNotSupportLabel}
diff --git a/webview-ui/src/components/settings/PromptsSettings.tsx b/webview-ui/src/components/settings/PromptsSettings.tsx index ee112b54dd..d09b53fe47 100644 --- a/webview-ui/src/components/settings/PromptsSettings.tsx +++ b/webview-ui/src/components/settings/PromptsSettings.tsx @@ -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 = ({ <>
{ const value = e.target.checked setIncludeTaskHistoryInEnhance(value) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 9866c1e235..2738b82632 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -341,7 +341,7 @@ const SettingsView = forwardRef(({ 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 }) diff --git a/webview-ui/src/components/settings/constants.ts b/webview-ui/src/components/settings/constants.ts index 5882b1bf4a..cdeb71814d 100644 --- a/webview-ui/src/components/settings/constants.ts +++ b/webview-ui/src/components/settings/constants.ts @@ -18,6 +18,8 @@ import { doubaoModels, internationalZAiModels, fireworksModels, + rooModels, + featherlessModels, } from "@roo-code/types" export const MODELS_BY_PROVIDER: Partial>> = { @@ -38,6 +40,8 @@ export const MODELS_BY_PROVIDER: Partial a.label.localeCompare(b.label)) diff --git a/webview-ui/src/components/settings/providers/Featherless.tsx b/webview-ui/src/components/settings/providers/Featherless.tsx new file mode 100644 index 0000000000..264e295dcc --- /dev/null +++ b/webview-ui/src/components/settings/providers/Featherless.tsx @@ -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( + ( + field: K, + transform: (event: E) => ProviderSettings[K] = inputEventTransform, + ) => + (event: E | Event) => { + setApiConfigurationField(field, transform(event as E)) + }, + [setApiConfigurationField], + ) + + return ( + <> + + + +
+ {t("settings:providers.apiKeyStorageNotice")} +
+ {!apiConfiguration?.featherlessApiKey && ( + + {t("settings:providers.getFeatherlessApiKey")} + + )} + + ) +} diff --git a/webview-ui/src/components/settings/providers/index.ts b/webview-ui/src/components/settings/providers/index.ts index f054780b06..eff33e1298 100644 --- a/webview-ui/src/components/settings/providers/index.ts +++ b/webview-ui/src/components/settings/providers/index.ts @@ -26,3 +26,4 @@ export { XAI } from "./XAI" export { ZAi } from "./ZAi" export { LiteLLM } from "./LiteLLM" export { Fireworks } from "./Fireworks" +export { Featherless } from "./Featherless" diff --git a/webview-ui/src/components/ui/hooks/useSelectedModel.ts b/webview-ui/src/components/ui/hooks/useSelectedModel.ts index b188f3e342..75a4a968ad 100644 --- a/webview-ui/src/components/ui/hooks/useSelectedModel.ts +++ b/webview-ui/src/components/ui/hooks/useSelectedModel.ts @@ -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": diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index ad35452079..b0045977c3 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -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 })), diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index ede37e530e..e67ef6d0fb 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -265,6 +265,12 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} Llançat", + "stealthModel": { + "feature": "Model stealth GRATUÏT per temps limitat - 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 roo/sonic del proveïdor Roo Code Cloud a
Configuració 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": "Cua de Missatges: 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..." diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 5964798684..d9c7ce7ee2 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -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", diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 0574da2df3..4994a15fab 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -265,6 +265,12 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} veröffentlicht", + "stealthModel": { + "feature": "Zeitlich begrenztes KOSTENLOSES Stealth-Modell - 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 roo/sonic vom Roo Code Cloud Provider in
Einstellungen 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": "Nachrichten-Warteschlange: 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" diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index 28f3e847fa..7f09401e57 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -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", diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 3479cdf69b..b3425d598d 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -274,13 +274,12 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} Released", - "description": "Roo Code {{version}} brings powerful new features and significant improvements to enhance your development workflow.", - "whatsNew": "What's New", - "feature1": "Message Queueing: Queue multiple messages while Roo is working, allowing you to continue planning your workflow without interruption.", - "feature2": "Custom Slash Commands: Create personalized slash commands for quick access to frequently used prompts and workflows, with full UI management.", - "feature3": "Enhanced Gemini Tools: New URL context and Google Search grounding capabilities provide Gemini models with real-time web information and enhanced research abilities.", - "hideButton": "Hide announcement", - "detailsDiscussLinks": "Get more details and discuss in Discord and Reddit 🚀" + "stealthModel": { + "feature": "Limited-time FREE stealth model - A blazing fast reasoning model that excels at agentic coding with a 262k context window, available through Roo Code Cloud.", + "note": "(Note: prompts and completions are logged by the model creator to improve the model)", + "connectButton": "Connect to Roo Code Cloud", + "selectModel": "Select roo/sonic from the Roo Code Cloud provider in
Settings to get started" + } }, "reasoning": { "thinking": "Thinking", @@ -352,9 +351,9 @@ "ariaLabel": "Version {{version}} - Click to view release notes" }, "rooCloudCTA": { - "title": "Roo Code Cloud is coming soon!", + "title": "Roo Code Cloud is evolving!", "description": "Run Roomote agents in the cloud, access your tasks from anywhere, collaborate with others, and more.", - "joinWaitlist": "Join the waitlist to get early access." + "joinWaitlist": "Sign up to get the latest updates." }, "command": { "triggerDescription": "Trigger the {{name}} command" diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index fca3d1ade9..d18a3bbd5e 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -268,6 +268,8 @@ "getChutesApiKey": "Get Chutes API Key", "fireworksApiKey": "Fireworks API Key", "getFireworksApiKey": "Get Fireworks API Key", + "featherlessApiKey": "Featherless API Key", + "getFeatherlessApiKey": "Get Featherless API Key", "ioIntelligenceApiKey": "IO Intelligence API Key", "ioIntelligenceApiKeyPlaceholder": "Enter your IO Intelligence API key", "getIoIntelligenceApiKey": "Get IO Intelligence API Key", @@ -376,6 +378,10 @@ "description": "No API key is required, but the user needs to help copy and paste the information to the web chat AI.", "instructions": "During use, a dialog box will pop up and the current message will be copied to the clipboard automatically. You need to paste these to web versions of AI (such as ChatGPT or Claude), then copy the AI's reply back to the dialog box and click the confirm button." }, + "roo": { + "authenticatedMessage": "Securely authenticated through your Roo Code Cloud account.", + "connectButton": "Connect to Roo Code Cloud" + }, "openRouter": { "providerRouting": { "title": "OpenRouter Provider Routing", diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 3621aa16cf..7d47d383bc 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -265,6 +265,12 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} publicado", + "stealthModel": { + "feature": "Modelo stealth GRATUITO por tiempo limitado - Un modelo de razonamiento ultrarrápido que sobresale en codificación agéntica con una ventana de contexto de 262k, disponible a través de Roo Code Cloud.", + "note": "(Nota: los prompts y completaciones son registrados por el creador del modelo y utilizados para mejorarlo)", + "connectButton": "Conectar a Roo Code Cloud", + "selectModel": "Selecciona roo/sonic del proveedor Roo Code Cloud en
Configuración para comenzar" + }, "description": "Roo Code {{version}} trae poderosas nuevas funcionalidades y mejoras significativas para mejorar tu flujo de trabajo de desarrollo.", "whatsNew": "Novedades", "feature1": "Cola de Mensajes: Pon en cola múltiples mensajes mientras Roo está trabajando, permitiéndote continuar planificando tu flujo de trabajo sin interrupciones.", @@ -349,9 +355,9 @@ "ariaLabel": "Versión {{version}} - Haz clic para ver las notas de la versión" }, "rooCloudCTA": { - "title": "¡Roo Code Cloud llegará pronto!", + "title": "¡Roo Code Cloud está evolucionando!", "description": "Ejecuta agentes remotos en la nube, accede a tus tareas desde cualquier lugar, colabora con otros y mucho más.", - "joinWaitlist": "Únete a la lista de espera para obtener acceso anticipado." + "joinWaitlist": "Regístrate para recibir las últimas actualizaciones." }, "editMessage": { "placeholder": "Edita tu mensaje..." diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index fd6e1e2715..ec9795d8b0 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Obtener clave API de Chutes", "fireworksApiKey": "Clave API de Fireworks", "getFireworksApiKey": "Obtener clave API de Fireworks", + "featherlessApiKey": "Clave API de Featherless", + "getFeatherlessApiKey": "Obtener clave API de Featherless", "ioIntelligenceApiKey": "Clave API de IO Intelligence", "ioIntelligenceApiKeyPlaceholder": "Introduce tu clave de API de IO Intelligence", "getIoIntelligenceApiKey": "Obtener clave API de IO Intelligence", @@ -377,6 +379,10 @@ "description": "No se requiere clave API, pero el usuario necesita ayudar a copiar y pegar la información en el chat web de IA.", "instructions": "Durante el uso, aparecerá un cuadro de diálogo y el mensaje actual se copiará automáticamente al portapapeles. Debe pegarlo en las versiones web de IA (como ChatGPT o Claude), luego copiar la respuesta de la IA de vuelta al cuadro de diálogo y hacer clic en el botón de confirmar." }, + "roo": { + "authenticatedMessage": "Autenticado de forma segura a través de tu cuenta de Roo Code Cloud.", + "connectButton": "Conectar a Roo Code Cloud" + }, "openRouter": { "providerRouting": { "title": "Enrutamiento de Proveedores de OpenRouter", diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 41ea4dd5a1..e12c9bb5e8 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -265,6 +265,12 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} est sortie", + "stealthModel": { + "feature": "Modèle stealth GRATUIT pour une durée limitée - Un modèle de raisonnement ultra-rapide qui excelle dans le codage agentique avec une fenêtre de contexte de 262k, disponible via Roo Code Cloud.", + "note": "(Note : les prompts et complétions sont enregistrés par le créateur du modèle et utilisés pour l'améliorer)", + "connectButton": "Se connecter à Roo Code Cloud", + "selectModel": "Sélectionne roo/sonic du fournisseur Roo Code Cloud dans
Paramètres pour commencer" + }, "description": "Roo Code {{version}} apporte de puissantes nouvelles fonctionnalités et des améliorations significatives pour améliorer ton flux de travail de développement.", "whatsNew": "Quoi de neuf", "feature1": "File d'Attente de Messages : Mettez en file d'attente plusieurs messages pendant que Roo travaille, vous permettant de continuer à planifier votre flux de travail sans interruption.", @@ -349,9 +355,9 @@ "ariaLabel": "Version {{version}} - Cliquez pour voir les notes de version" }, "rooCloudCTA": { - "title": "Roo Code Cloud arrive bientôt !", + "title": "Roo Code Cloud évolue !", "description": "Exécutez des agents distants dans le cloud, accédez à vos tâches de n'importe où, collaborez avec d'autres et bien plus encore.", - "joinWaitlist": "Rejoignez la liste d'attente pour obtenir un accès anticipé." + "joinWaitlist": "Inscrivez-vous pour recevoir les dernières mises à jour." }, "editMessage": { "placeholder": "Modifiez votre message..." diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 451e51d084..68230b1a60 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Obtenir la clé API Chutes", "fireworksApiKey": "Clé API Fireworks", "getFireworksApiKey": "Obtenir la clé API Fireworks", + "featherlessApiKey": "Clé API Featherless", + "getFeatherlessApiKey": "Obtenir la clé API Featherless", "ioIntelligenceApiKey": "Clé API IO Intelligence", "ioIntelligenceApiKeyPlaceholder": "Saisissez votre clé d'API IO Intelligence", "getIoIntelligenceApiKey": "Obtenir la clé API IO Intelligence", @@ -377,6 +379,10 @@ "description": "Aucune clé API n'est requise, mais l'utilisateur doit aider à copier et coller les informations dans le chat web de l'IA.", "instructions": "Pendant l'utilisation, une boîte de dialogue apparaîtra et le message actuel sera automatiquement copié dans le presse-papiers. Vous devez le coller dans les versions web de l'IA (comme ChatGPT ou Claude), puis copier la réponse de l'IA dans la boîte de dialogue et cliquer sur le bouton de confirmation." }, + "roo": { + "authenticatedMessage": "Authentifié de manière sécurisée via ton compte Roo Code Cloud.", + "connectButton": "Se connecter à Roo Code Cloud" + }, "openRouter": { "providerRouting": { "title": "Routage des fournisseurs OpenRouter", diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 05288792cf..ef1edf7796 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -265,6 +265,12 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} रिलीज़ हुआ", + "stealthModel": { + "feature": "सीमित समय के लिए मुफ़्त स्टेल्थ मॉडल - एक अत्यंत तेज़ रीज़निंग मॉडल जो 262k कॉन्टेक्स्ट विंडो के साथ एजेंटिक कोडिंग में उत्कृष्ट है, Roo Code Cloud के माध्यम से उपलब्ध।", + "note": "(नोट: प्रॉम्प्ट्स और कम्प्लीशन्स मॉडल निर्माता द्वारा लॉग किए जाते हैं और मॉडल को बेहतर बनाने के लिए उपयोग किए जाते हैं)", + "connectButton": "Roo Code Cloud से कनेक्ट करें", + "selectModel": "
सेटिंग्स में Roo Code Cloud प्रोवाइडर से roo/sonic चुनें और शुरू करें" + }, "description": "Roo Code {{version}} आपके विकास वर्कफ़्लो को बेहतर बनाने के लिए शक्तिशाली नई सुविधाएं और महत्वपूर्ण सुधार लेकर आया है।", "whatsNew": "नया क्या है", "feature1": "संदेश कतार: Roo के काम करते समय कई संदेशों को कतार में रखें, जिससे आप बिना रुकावट के अपने वर्कफ़्लो की योजना बना सकते हैं।", @@ -349,9 +355,9 @@ "ariaLabel": "संस्करण {{version}} - रिलीज़ नोट्स देखने के लिए क्लिक करें" }, "rooCloudCTA": { - "title": "Roo Code Cloud जल्द आ रहा है!", + "title": "Roo Code Cloud विकसित हो रहा है!", "description": "क्लाउड में रिमोट एजेंट चलाएं, कहीं से भी अपने कार्यों तक पहुंचें, दूसरों के साथ सहयोग करें, और बहुत कुछ।", - "joinWaitlist": "जल्दी पहुंच पाने के लिए प्रतीक्षा सूची में शामिल हों।" + "joinWaitlist": "नवीनतम अपडेट प्राप्त करने के लिए साइन अप करें।" }, "editMessage": { "placeholder": "अपना संदेश संपादित करें..." diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 83a4b7b81b..f98f7e6510 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Chutes API कुंजी प्राप्त करें", "fireworksApiKey": "Fireworks API कुंजी", "getFireworksApiKey": "Fireworks API कुंजी प्राप्त करें", + "featherlessApiKey": "Featherless API कुंजी", + "getFeatherlessApiKey": "Featherless API कुंजी प्राप्त करें", "ioIntelligenceApiKey": "IO Intelligence API कुंजी", "ioIntelligenceApiKeyPlaceholder": "अपना आईओ इंटेलिजेंस एपीआई कुंजी दर्ज करें", "getIoIntelligenceApiKey": "IO Intelligence API कुंजी प्राप्त करें", @@ -377,6 +379,10 @@ "description": "कोई API कुंजी आवश्यक नहीं है, लेकिन उपयोगकर्ता को वेब चैट AI में जानकारी कॉपी और पेस्ट करने में मदद करनी होगी।", "instructions": "उपयोग के दौरान, एक डायलॉग बॉक्स पॉप अप होगा और वर्तमान संदेश स्वचालित रूप से क्लिपबोर्ड पर कॉपी हो जाएगा। आपको इन्हें AI के वेब संस्करणों (जैसे ChatGPT या Claude) में पेस्ट करना होगा, फिर AI की प्रतिक्रिया को डायलॉग बॉक्स में वापस कॉपी करें और पुष्टि बटन पर क्लिक करें।" }, + "roo": { + "authenticatedMessage": "आपके Roo Code Cloud खाते के माध्यम से सुरक्षित रूप से प्रमाणित।", + "connectButton": "Roo Code Cloud से कनेक्ट करें" + }, "openRouter": { "providerRouting": { "title": "OpenRouter प्रदाता रूटिंग", diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index 909606ee91..9db1acce02 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -277,6 +277,12 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} Dirilis", + "stealthModel": { + "feature": "Model stealth GRATIS waktu terbatas - Model penalaran super cepat yang unggul dalam coding agentik dengan jendela konteks 262k, tersedia melalui Roo Code Cloud.", + "note": "(Catatan: prompt dan completion dicatat oleh pembuat model dan digunakan untuk meningkatkan model)", + "connectButton": "Hubungkan ke Roo Code Cloud", + "selectModel": "Pilih roo/sonic dari penyedia Roo Code Cloud di
Pengaturan untuk memulai" + }, "description": "Roo Code {{version}} menghadirkan fitur-fitur baru yang kuat dan peningkatan signifikan untuk meningkatkan alur kerja pengembangan Anda.", "whatsNew": "Yang Baru", "feature1": "Antrian Pesan: Antrikan beberapa pesan saat Roo sedang bekerja, memungkinkan Anda melanjutkan perencanaan alur kerja tanpa gangguan.", @@ -355,9 +361,9 @@ "ariaLabel": "Versi {{version}} - Klik untuk melihat catatan rilis" }, "rooCloudCTA": { - "title": "Roo Code Cloud segera hadir!", + "title": "Roo Code Cloud sedang berkembang!", "description": "Jalankan agen jarak jauh di cloud, akses tugas Anda dari mana saja, berkolaborasi dengan orang lain, dan banyak lagi.", - "joinWaitlist": "Bergabunglah dengan daftar tunggu untuk mendapatkan akses awal." + "joinWaitlist": "Daftar untuk mendapatkan pembaruan terbaru." }, "editMessage": { "placeholder": "Edit pesan Anda..." diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index fd7edf40cf..748bf198eb 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -273,6 +273,8 @@ "getChutesApiKey": "Dapatkan Chutes API Key", "fireworksApiKey": "Fireworks API Key", "getFireworksApiKey": "Dapatkan Fireworks API Key", + "featherlessApiKey": "Featherless API Key", + "getFeatherlessApiKey": "Dapatkan Featherless API Key", "ioIntelligenceApiKey": "IO Intelligence API Key", "ioIntelligenceApiKeyPlaceholder": "Masukkan kunci API IO Intelligence Anda", "getIoIntelligenceApiKey": "Dapatkan IO Intelligence API Key", @@ -381,6 +383,10 @@ "description": "Tidak diperlukan API key, tetapi pengguna perlu membantu menyalin dan menempel informasi ke web chat AI.", "instructions": "Selama penggunaan, kotak dialog akan muncul dan pesan saat ini akan disalin ke clipboard secara otomatis. Kamu perlu menempel ini ke versi web AI (seperti ChatGPT atau Claude), lalu salin balasan AI kembali ke kotak dialog dan klik tombol konfirmasi." }, + "roo": { + "authenticatedMessage": "Terautentikasi dengan aman melalui akun Roo Code Cloud Anda.", + "connectButton": "Hubungkan ke Roo Code Cloud" + }, "openRouter": { "providerRouting": { "title": "OpenRouter Provider Routing", diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 480cc13fad..8b2d992915 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -265,6 +265,12 @@ }, "announcement": { "title": "🎉 Rilasciato Roo Code {{version}}", + "stealthModel": { + "feature": "Modello stealth GRATUITO per tempo limitato - Un modello di ragionamento velocissimo che eccelle nella programmazione agentica con una finestra di contesto di 262k, disponibile tramite Roo Code Cloud.", + "note": "(Nota: i prompt e le completazioni sono registrati dal creatore del modello e utilizzati per migliorarlo)", + "connectButton": "Connetti a Roo Code Cloud", + "selectModel": "Seleziona roo/sonic dal provider Roo Code Cloud in
Impostazioni per iniziare" + }, "description": "Roo Code {{version}} porta nuove potenti funzionalità e miglioramenti significativi per potenziare il tuo flusso di lavoro di sviluppo.", "whatsNew": "Novità", "feature1": "Coda Messaggi: Metti in coda più messaggi mentre Roo sta lavorando, permettendoti di continuare a pianificare il tuo flusso di lavoro senza interruzioni.", @@ -349,9 +355,9 @@ "ariaLabel": "Versione {{version}} - Clicca per visualizzare le note di rilascio" }, "rooCloudCTA": { - "title": "Roo Code Cloud arriva presto!", + "title": "Roo Code Cloud si sta evolvendo!", "description": "Esegui agenti remoti nel cloud, accedi alle tue attività da qualsiasi luogo, collabora con altri e molto altro.", - "joinWaitlist": "Unisciti alla lista d'attesa per ottenere l'accesso anticipato." + "joinWaitlist": "Registrati per ricevere gli ultimi aggiornamenti." }, "editMessage": { "placeholder": "Modifica il tuo messaggio..." diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index c8a8800e4f..b97d96a61b 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Ottieni chiave API Chutes", "fireworksApiKey": "Chiave API Fireworks", "getFireworksApiKey": "Ottieni chiave API Fireworks", + "featherlessApiKey": "Chiave API Featherless", + "getFeatherlessApiKey": "Ottieni chiave API Featherless", "ioIntelligenceApiKey": "Chiave API IO Intelligence", "ioIntelligenceApiKeyPlaceholder": "Inserisci la tua chiave API IO Intelligence", "getIoIntelligenceApiKey": "Ottieni chiave API IO Intelligence", @@ -377,6 +379,10 @@ "description": "Non è richiesta alcuna chiave API, ma l'utente dovrà aiutare a copiare e incollare le informazioni nella chat web AI.", "instructions": "Durante l'uso, apparirà una finestra di dialogo e il messaggio corrente verrà automaticamente copiato negli appunti. Dovrai incollarlo nelle versioni web dell'AI (come ChatGPT o Claude), quindi copiare la risposta dell'AI nella finestra di dialogo e fare clic sul pulsante di conferma." }, + "roo": { + "authenticatedMessage": "Autenticato in modo sicuro tramite il tuo account Roo Code Cloud.", + "connectButton": "Connetti a Roo Code Cloud" + }, "openRouter": { "providerRouting": { "title": "Routing dei fornitori OpenRouter", diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index 6b3340e7f0..7008c8b8f8 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -265,6 +265,12 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} リリース", + "stealthModel": { + "feature": "期間限定無料ステルスモデル - 262kコンテキストウィンドウを持つ、エージェンティックコーディングに優れた超高速推論モデル、Roo Code Cloud経由で利用可能。", + "note": "(注意:プロンプトと補完はモデル作成者によってログに記録され、モデルの改善に使用されます)", + "connectButton": "Roo Code Cloudに接続", + "selectModel": "
設定でRoo Code Cloudプロバイダーからroo/sonicを選択して開始" + }, "description": "Roo Code {{version}}は、開発ワークフローを向上させる強力な新機能と重要な改善をもたらします。", "whatsNew": "新機能", "feature1": "メッセージキュー: Rooが作業中に複数のメッセージをキューに入れ、ワークフローの計画を中断することなく続行できます。", @@ -349,9 +355,9 @@ "ariaLabel": "バージョン {{version}} - クリックしてリリースノートを表示" }, "rooCloudCTA": { - "title": "Roo Code Cloud が間もなく登場!", + "title": "Roo Code Cloud が進化中!", "description": "クラウドでリモートエージェントを実行し、どこからでもタスクにアクセスし、他の人と協力し、その他多くの機能を利用できます。", - "joinWaitlist": "早期アクセスを取得するためにウェイトリストに参加してください。" + "joinWaitlist": "最新のアップデートを受け取るためにサインアップしてください。" }, "editMessage": { "placeholder": "メッセージを編集..." diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index d3e7b04baf..8061418d38 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Chutes APIキーを取得", "fireworksApiKey": "Fireworks APIキー", "getFireworksApiKey": "Fireworks APIキーを取得", + "featherlessApiKey": "Featherless APIキー", + "getFeatherlessApiKey": "Featherless APIキーを取得", "ioIntelligenceApiKey": "IO Intelligence APIキー", "ioIntelligenceApiKeyPlaceholder": "IO Intelligence APIキーを入力してください", "getIoIntelligenceApiKey": "IO Intelligence APIキーを取得", @@ -377,6 +379,10 @@ "description": "APIキーは不要ですが、ユーザーはウェブチャットAIに情報をコピー&ペーストする必要があります。", "instructions": "使用中にダイアログボックスが表示され、現在のメッセージが自動的にクリップボードにコピーされます。これらをウェブ版のAI(ChatGPTやClaudeなど)に貼り付け、AIの返答をダイアログボックスにコピーして確認ボタンをクリックする必要があります。" }, + "roo": { + "authenticatedMessage": "Roo Code Cloudアカウントを通じて安全に認証されています。", + "connectButton": "Roo Code Cloudに接続" + }, "openRouter": { "providerRouting": { "title": "OpenRouterプロバイダールーティング", diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 4b8d652f03..46f204ed68 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -265,6 +265,12 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} 출시", + "stealthModel": { + "feature": "기간 한정 무료 스텔스 모델 - 262k 컨텍스트 윈도우를 가진 에이전틱 코딩에 뛰어난 초고속 추론 모델, Roo Code Cloud를 통해 이용 가능.", + "note": "(참고: 프롬프트와 완성은 모델 제작자에 의해 기록되고 모델 개선에 사용됩니다)", + "connectButton": "Roo Code Cloud에 연결", + "selectModel": "
설정에서 Roo Code Cloud 제공업체의 roo/sonic을 선택하여 시작" + }, "description": "Roo Code {{version}}은 개발 워크플로우를 향상시키는 강력한 새 기능과 중요한 개선사항을 제공합니다.", "whatsNew": "새로운 기능", "feature1": "메시지 대기열: Roo가 작업하는 동안 여러 메시지를 대기열에 넣어 워크플로우 계획을 중단 없이 계속할 수 있습니다.", @@ -349,9 +355,9 @@ "ariaLabel": "버전 {{version}} - 릴리스 노트를 보려면 클릭하세요" }, "rooCloudCTA": { - "title": "Roo Code Cloud가 곧 출시됩니다!", + "title": "Roo Code Cloud가 진화하고 있습니다!", "description": "클라우드에서 원격 에이전트를 실행하고, 어디서나 작업에 액세스하고, 다른 사람들과 협업하는 등 다양한 기능을 이용하세요.", - "joinWaitlist": "얼리 액세스를 받으려면 대기 목록에 가입하세요." + "joinWaitlist": "최신 업데이트를 받으려면 가입하세요." }, "editMessage": { "placeholder": "메시지 편집..." diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index a5bcd1f385..08f3a8c4e7 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Chutes API 키 받기", "fireworksApiKey": "Fireworks API 키", "getFireworksApiKey": "Fireworks API 키 받기", + "featherlessApiKey": "Featherless API 키", + "getFeatherlessApiKey": "Featherless API 키 받기", "ioIntelligenceApiKey": "IO Intelligence API 키", "ioIntelligenceApiKeyPlaceholder": "IO Intelligence API 키를 입력하세요", "getIoIntelligenceApiKey": "IO Intelligence API 키 받기", @@ -377,6 +379,10 @@ "description": "API 키가 필요하지 않지만, 사용자가 웹 채팅 AI에 정보를 복사하여 붙여넣어야 합니다.", "instructions": "사용 중에 대화 상자가 나타나고 현재 메시지가 자동으로 클립보드에 복사됩니다. 이를 웹 버전 AI(예: ChatGPT 또는 Claude)에 붙여넣은 다음, AI의 응답을 대화 상자에 복사하고 확인 버튼을 클릭해야 합니다." }, + "roo": { + "authenticatedMessage": "Roo Code Cloud 계정을 통해 안전하게 인증되었습니다.", + "connectButton": "Roo Code Cloud에 연결" + }, "openRouter": { "providerRouting": { "title": "OpenRouter 제공자 라우팅", diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 871ab1371e..061d12269c 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -250,6 +250,12 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} uitgebracht", + "stealthModel": { + "feature": "Beperkt tijd GRATIS stealth model - Een bliksemsnelle redeneermodel die uitblinkt in agentische programmering met een 262k contextvenster, beschikbaar via Roo Code Cloud.", + "note": "(Opmerking: prompts en aanvullingen worden gelogd door de modelmaker en gebruikt om het model te verbeteren)", + "connectButton": "Verbinden met Roo Code Cloud", + "selectModel": "Selecteer roo/sonic van de Roo Code Cloud provider in
Instellingen om te beginnen" + }, "description": "Roo Code {{version}} brengt krachtige nieuwe functies en significante verbeteringen om je ontwikkelingsworkflow te verbeteren.", "whatsNew": "Wat is er nieuw", "feature1": "Berichtenwachtrij: Zet meerdere berichten in de wachtrij terwijl Roo werkt, zodat je je workflow kunt blijven plannen zonder onderbreking.", @@ -349,9 +355,9 @@ "ariaLabel": "Versie {{version}} - Klik om release notes te bekijken" }, "rooCloudCTA": { - "title": "Roo Code Cloud komt binnenkort!", + "title": "Roo Code Cloud evolueert!", "description": "Voer externe agenten uit in de cloud, krijg overal toegang tot je taken, werk samen met anderen en nog veel meer.", - "joinWaitlist": "Sluit je aan bij de wachtlijst voor vroege toegang." + "joinWaitlist": "Meld je aan om de laatste updates te ontvangen." }, "editMessage": { "placeholder": "Bewerk je bericht..." diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index b54e021be0..cc4eb2c90f 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Chutes API-sleutel ophalen", "fireworksApiKey": "Fireworks API-sleutel", "getFireworksApiKey": "Fireworks API-sleutel ophalen", + "featherlessApiKey": "Featherless API-sleutel", + "getFeatherlessApiKey": "Featherless API-sleutel ophalen", "ioIntelligenceApiKey": "IO Intelligence API-sleutel", "ioIntelligenceApiKeyPlaceholder": "Voer je IO Intelligence API-sleutel in", "getIoIntelligenceApiKey": "IO Intelligence API-sleutel ophalen", @@ -377,6 +379,10 @@ "description": "Geen API-sleutel vereist, maar de gebruiker moet helpen met kopiëren en plakken naar de webchat-AI.", "instructions": "Tijdens gebruik verschijnt een dialoogvenster en wordt het huidige bericht automatisch naar het klembord gekopieerd. Je moet deze plakken in webversies van AI (zoals ChatGPT of Claude), vervolgens het antwoord van de AI terugkopiëren naar het dialoogvenster en op bevestigen klikken." }, + "roo": { + "authenticatedMessage": "Veilig geauthenticeerd via je Roo Code Cloud-account.", + "connectButton": "Verbinden met Roo Code Cloud" + }, "openRouter": { "providerRouting": { "title": "OpenRouter-providerroutering", diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index 5a06dc3091..b997cc09ff 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -265,6 +265,12 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} wydany", + "stealthModel": { + "feature": "Darmowy model stealth na ograniczony czas - Błyskawiczny model rozumowania, który doskonale radzi sobie z kodowaniem agentowym z oknem kontekstu 262k, dostępny przez Roo Code Cloud.", + "note": "(Uwaga: prompty i uzupełnienia są rejestrowane przez twórcę modelu i używane do jego ulepszania)", + "connectButton": "Połącz z Roo Code Cloud", + "selectModel": "Wybierz roo/sonic od dostawcy Roo Code Cloud w
Ustawieniach aby rozpocząć" + }, "description": "Roo Code {{version}} wprowadza potężne nowe funkcje i znaczące ulepszenia, aby ulepszyć Twój przepływ pracy programistycznej.", "whatsNew": "Co nowego", "feature1": "Kolejka Wiadomości: Umieszczaj wiele wiadomości w kolejce podczas pracy Roo, pozwalając na kontynuowanie planowania przepływu pracy bez przerw.", @@ -349,9 +355,9 @@ "ariaLabel": "Wersja {{version}} - Kliknij, aby wyświetlić informacje o wydaniu" }, "rooCloudCTA": { - "title": "Roo Code Cloud już wkrótce!", + "title": "Roo Code Cloud się rozwija!", "description": "Uruchamiaj zdalne agenty w chmurze, uzyskuj dostęp do swoich zadań z dowolnego miejsca, współpracuj z innymi i wiele więcej.", - "joinWaitlist": "Dołącz do listy oczekujących, aby uzyskać wczesny dostęp." + "joinWaitlist": "Zarejestruj się, aby otrzymywać najnowsze aktualizacje." }, "editMessage": { "placeholder": "Edytuj swoją wiadomość..." diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 194bd9029d..107be09fdd 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Uzyskaj klucz API Chutes", "fireworksApiKey": "Klucz API Fireworks", "getFireworksApiKey": "Uzyskaj klucz API Fireworks", + "featherlessApiKey": "Klucz API Featherless", + "getFeatherlessApiKey": "Uzyskaj klucz API Featherless", "ioIntelligenceApiKey": "Klucz API IO Intelligence", "ioIntelligenceApiKeyPlaceholder": "Wprowadź swój klucz API IO Intelligence", "getIoIntelligenceApiKey": "Uzyskaj klucz API IO Intelligence", @@ -377,6 +379,10 @@ "description": "Nie jest wymagany klucz API, ale użytkownik będzie musiał pomóc w kopiowaniu i wklejaniu informacji do czatu internetowego AI.", "instructions": "Podczas użytkowania pojawi się okno dialogowe, a bieżąca wiadomość zostanie automatycznie skopiowana do schowka. Będziesz musiał wkleić ją do internetowych wersji AI (takich jak ChatGPT lub Claude), a następnie skopiować odpowiedź AI z powrotem do okna dialogowego i kliknąć przycisk potwierdzenia." }, + "roo": { + "authenticatedMessage": "Bezpiecznie uwierzytelniony przez twoje konto Roo Code Cloud.", + "connectButton": "Połącz z Roo Code Cloud" + }, "openRouter": { "providerRouting": { "title": "Routing dostawców OpenRouter", diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 65854c508d..5e09955cb8 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -265,6 +265,12 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} Lançado", + "stealthModel": { + "feature": "Modelo stealth GRATUITO por tempo limitado - Um modelo de raciocínio ultrarrápido que se destaca em codificação agêntica com uma janela de contexto de 262k, disponível através do Roo Code Cloud.", + "note": "(Nota: prompts e completações são registrados pelo criador do modelo e usados para melhorá-lo)", + "connectButton": "Conectar ao Roo Code Cloud", + "selectModel": "Selecione roo/sonic do provedor Roo Code Cloud em
Configurações para começar" + }, "description": "Roo Code {{version}} traz novos recursos poderosos e melhorias significativas para aprimorar seu fluxo de trabalho de desenvolvimento.", "whatsNew": "O que há de novo", "feature1": "Fila de Mensagens: Coloque várias mensagens na fila enquanto o Roo está trabalhando, permitindo que você continue planejando seu fluxo de trabalho sem interrupção.", @@ -349,9 +355,9 @@ "ariaLabel": "Versão {{version}} - Clique para ver as notas de lançamento" }, "rooCloudCTA": { - "title": "Roo Code Cloud chegará em breve!", + "title": "Roo Code Cloud está evoluindo!", "description": "Execute agentes remotos na nuvem, acesse suas tarefas de qualquer lugar, colabore com outros e muito mais.", - "joinWaitlist": "Junte-se à lista de espera para obter acesso antecipado." + "joinWaitlist": "Cadastre-se para receber as últimas atualizações." }, "editMessage": { "placeholder": "Edite sua mensagem..." diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 7879dd5154..54343a2fc5 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Obter chave de API Chutes", "fireworksApiKey": "Chave de API Fireworks", "getFireworksApiKey": "Obter chave de API Fireworks", + "featherlessApiKey": "Chave de API Featherless", + "getFeatherlessApiKey": "Obter chave de API Featherless", "ioIntelligenceApiKey": "Chave de API IO Intelligence", "ioIntelligenceApiKeyPlaceholder": "Insira sua chave de API da IO Intelligence", "getIoIntelligenceApiKey": "Obter chave de API IO Intelligence", @@ -377,6 +379,10 @@ "description": "Não é necessária chave de API, mas o usuário precisa ajudar a copiar e colar as informações para a IA do chat web.", "instructions": "Durante o uso, uma caixa de diálogo será exibida e a mensagem atual será copiada para a área de transferência automaticamente. Você precisa colar isso nas versões web de IA (como ChatGPT ou Claude), depois copiar a resposta da IA de volta para a caixa de diálogo e clicar no botão confirmar." }, + "roo": { + "authenticatedMessage": "Autenticado com segurança através da sua conta Roo Code Cloud.", + "connectButton": "Conectar ao Roo Code Cloud" + }, "openRouter": { "providerRouting": { "title": "Roteamento de Provedores OpenRouter", diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index ff43851495..fb5a1331e2 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -250,6 +250,12 @@ }, "announcement": { "title": "🎉 Выпущен Roo Code {{version}}", + "stealthModel": { + "feature": "Бесплатная скрытая модель на ограниченное время - Сверхбыстрая модель рассуждений, которая превосходно справляется с агентным программированием с окном контекста 262k, доступна через Roo Code Cloud.", + "note": "(Примечание: промпты и дополнения записываются создателем модели и используются для её улучшения)", + "connectButton": "Подключиться к Roo Code Cloud", + "selectModel": "Выберите roo/sonic от провайдера Roo Code Cloud в
Настройках для начала" + }, "description": "Roo Code {{version}} приносит мощные новые функции и значительные улучшения для совершенствования вашего рабочего процесса разработки.", "whatsNew": "Что нового", "feature1": "Очередь сообщений: Ставьте несколько сообщений в очередь, пока Roo работает, позволяя вам продолжать планировать рабочий процесс без прерывания.", @@ -349,9 +355,9 @@ "ariaLabel": "Версия {{version}} - Нажмите, чтобы просмотреть примечания к выпуску" }, "rooCloudCTA": { - "title": "Roo Code Cloud скоро появится!", + "title": "Roo Code Cloud развивается!", "description": "Запускайте удаленные агенты в облаке, получайте доступ к своим задачам из любого места, сотрудничайте с другими и многое другое.", - "joinWaitlist": "Присоединитесь к списку ожидания для получения раннего доступа." + "joinWaitlist": "Зарегистрируйтесь, чтобы получать последние обновления." }, "editMessage": { "placeholder": "Редактировать сообщение..." diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 3744ecedff..ac5fafdc17 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Получить Chutes API-ключ", "fireworksApiKey": "Fireworks API-ключ", "getFireworksApiKey": "Получить Fireworks API-ключ", + "featherlessApiKey": "Featherless API-ключ", + "getFeatherlessApiKey": "Получить Featherless API-ключ", "ioIntelligenceApiKey": "IO Intelligence API-ключ", "ioIntelligenceApiKeyPlaceholder": "Введите свой ключ API IO Intelligence", "getIoIntelligenceApiKey": "Получить IO Intelligence API-ключ", @@ -377,6 +379,10 @@ "description": "API-ключ не требуется, но пользователю нужно вручную копировать и вставлять информацию в веб-чат ИИ.", "instructions": "Во время использования появится диалоговое окно, и текущее сообщение будет скопировано в буфер обмена автоматически. Вам нужно вставить его в веб-версию ИИ (например, ChatGPT или Claude), затем скопировать ответ ИИ обратно в диалоговое окно и нажать кнопку подтверждения." }, + "roo": { + "authenticatedMessage": "Безопасно аутентифицирован через твой аккаунт Roo Code Cloud.", + "connectButton": "Подключиться к Roo Code Cloud" + }, "openRouter": { "providerRouting": { "title": "Маршрутизация провайдера OpenRouter", diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index cf3dee847b..832c780c40 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -265,6 +265,12 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} Yayınlandı", + "stealthModel": { + "feature": "Sınırlı süre ÜCRETSİZ gizli model - 262k bağlam penceresi ile ajantik kodlamada mükemmel olan çok hızlı akıl yürütme modeli, Roo Code Cloud üzerinden kullanılabilir.", + "note": "(Not: istemler ve tamamlamalar model yaratıcısı tarafından kaydedilir ve modeli geliştirmek için kullanılır)", + "connectButton": "Roo Code Cloud'a bağlan", + "selectModel": "
Ayarlar'da Roo Code Cloud sağlayıcısından roo/sonic'i seç ve başla" + }, "description": "Roo Code {{version}}, geliştirme iş akışınızı geliştirmek için güçlü yeni özellikler ve önemli iyileştirmeler getiriyor.", "whatsNew": "Yenilikler", "feature1": "Mesaj Kuyruğu: Roo çalışırken birden fazla mesajı kuyruğa alın, iş akışınızı kesintisiz olarak planlamaya devam etmenizi sağlar.", @@ -349,9 +355,9 @@ "ariaLabel": "Sürüm {{version}} - Sürüm notlarını görüntülemek için tıklayın" }, "rooCloudCTA": { - "title": "Roo Code Cloud yakında geliyor!", + "title": "Roo Code Cloud gelişiyor!", "description": "Bulutta uzak ajanlar çalıştırın, görevlerinize her yerden erişin, başkalarıyla işbirliği yapın ve daha fazlası.", - "joinWaitlist": "Erken erişim için bekleme listesine katılın." + "joinWaitlist": "En son güncellemeleri almak için kaydolun." }, "editMessage": { "placeholder": "Mesajını düzenle..." diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index be1a7a0169..0c4138e783 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Chutes API Anahtarı Al", "fireworksApiKey": "Fireworks API Anahtarı", "getFireworksApiKey": "Fireworks API Anahtarı Al", + "featherlessApiKey": "Featherless API Anahtarı", + "getFeatherlessApiKey": "Featherless API Anahtarı Al", "ioIntelligenceApiKey": "IO Intelligence API Anahtarı", "ioIntelligenceApiKeyPlaceholder": "IO Intelligence API anahtarınızı girin", "getIoIntelligenceApiKey": "IO Intelligence API Anahtarı Al", @@ -377,6 +379,10 @@ "description": "API anahtarı gerekmez, ancak kullanıcının bilgileri web sohbet yapay zekasına kopyalayıp yapıştırması gerekir.", "instructions": "Kullanım sırasında bir iletişim kutusu açılacak ve mevcut mesaj otomatik olarak panoya kopyalanacaktır. Bunları web yapay zekalarına (ChatGPT veya Claude gibi) yapıştırmanız, ardından yapay zekanın yanıtını iletişim kutusuna kopyalayıp onay düğmesine tıklamanız gerekir." }, + "roo": { + "authenticatedMessage": "Roo Code Cloud hesabın üzerinden güvenli bir şekilde kimlik doğrulandı.", + "connectButton": "Roo Code Cloud'a Bağlan" + }, "openRouter": { "providerRouting": { "title": "OpenRouter Sağlayıcı Yönlendirmesi", diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index e1bf274511..1106f2d85c 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -265,6 +265,12 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} Đã phát hành", + "stealthModel": { + "feature": "Mô hình stealth MIỄN PHÍ có thời hạn - Một mô hình lý luận cực nhanh xuất sắc trong lập trình agentic với cửa sổ ngữ cảnh 262k, có sẵn qua Roo Code Cloud.", + "note": "(Lưu ý: các prompt và completion được ghi lại bởi người tạo mô hình và được sử dụng để cải thiện mô hình)", + "connectButton": "Kết nối với Roo Code Cloud", + "selectModel": "Chọn roo/sonic từ nhà cung cấp Roo Code Cloud trong
Cài đặt để bắt đầu" + }, "description": "Roo Code {{version}} mang đến các tính năng mạnh mẽ mới và cải tiến đáng kể để nâng cao quy trình phát triển của bạn.", "whatsNew": "Có gì mới", "feature1": "Hàng đợi Tin nhắn: Xếp hàng nhiều tin nhắn trong khi Roo đang làm việc, cho phép bạn tiếp tục lập kế hoạch quy trình làm việc mà không bị gián đoạn.", @@ -349,9 +355,9 @@ "ariaLabel": "Phiên bản {{version}} - Nhấp để xem ghi chú phát hành" }, "rooCloudCTA": { - "title": "Roo Code Cloud sắp ra mắt!", + "title": "Roo Code Cloud đang phát triển!", "description": "Chạy các agent từ xa trên cloud, truy cập các tác vụ của bạn từ mọi nơi, cộng tác với người khác và nhiều hơn nữa.", - "joinWaitlist": "Tham gia danh sách chờ để được truy cập sớm." + "joinWaitlist": "Đăng ký để nhận các cập nhật mới nhất." }, "editMessage": { "placeholder": "Chỉnh sửa tin nhắn của bạn..." diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 7526cf31a7..5b2a5a6ef8 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "Lấy khóa API Chutes", "fireworksApiKey": "Khóa API Fireworks", "getFireworksApiKey": "Lấy khóa API Fireworks", + "featherlessApiKey": "Khóa API Featherless", + "getFeatherlessApiKey": "Lấy khóa API Featherless", "ioIntelligenceApiKey": "Khóa API IO Intelligence", "ioIntelligenceApiKeyPlaceholder": "Nhập khóa API IO Intelligence của bạn", "getIoIntelligenceApiKey": "Lấy khóa API IO Intelligence", @@ -377,6 +379,10 @@ "description": "Không cần khóa API, nhưng người dùng cần giúp sao chép và dán thông tin vào AI trò chuyện web.", "instructions": "Trong quá trình sử dụng, một hộp thoại sẽ xuất hiện và tin nhắn hiện tại sẽ được tự động sao chép vào clipboard. Bạn cần dán chúng vào các phiên bản web của AI (như ChatGPT hoặc Claude), sau đó sao chép phản hồi của AI trở lại hộp thoại và nhấp vào nút xác nhận." }, + "roo": { + "authenticatedMessage": "Đã xác thực an toàn thông qua tài khoản Roo Code Cloud của bạn.", + "connectButton": "Kết nối với Roo Code Cloud" + }, "openRouter": { "providerRouting": { "title": "Định tuyến nhà cung cấp OpenRouter", diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 1d446b43f5..bb06f535eb 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -265,6 +265,12 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} 已发布", + "stealthModel": { + "feature": "限时免费隐形模型 - 一个在代理编程方面表现出色的超快推理模型,拥有 262k 上下文窗口,通过 Roo Code Cloud 提供。", + "note": "(注意:提示词和补全内容会被模型创建者记录并用于改进模型)", + "connectButton": "连接到 Roo Code Cloud", + "selectModel": "在
设置中从 Roo Code Cloud 提供商选择 roo/sonic 开始使用" + }, "description": "Roo Code {{version}} 带来强大的新功能和重大改进,提升您的开发工作流程。", "whatsNew": "新特性", "feature1": "消息队列: 在 Roo 工作时将多个消息排队,让你可以不间断地继续规划工作流程。", @@ -349,9 +355,9 @@ "ariaLabel": "版本 {{version}} - 点击查看发布说明" }, "rooCloudCTA": { - "title": "Roo Code Cloud 即将推出!", + "title": "Roo Code Cloud 正在进化!", "description": "在云端运行远程代理,随时随地访问任务,与他人协作等更多功能。", - "joinWaitlist": "加入等待列表获取早期访问权限。" + "joinWaitlist": "注册获取最新更新。" }, "command": { "triggerDescription": "触发 {{name}} 命令" diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index c1985e7490..9e56ba74ee 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "获取 Chutes API 密钥", "fireworksApiKey": "Fireworks API 密钥", "getFireworksApiKey": "获取 Fireworks API 密钥", + "featherlessApiKey": "Featherless API 密钥", + "getFeatherlessApiKey": "获取 Featherless API 密钥", "ioIntelligenceApiKey": "IO Intelligence API 密钥", "ioIntelligenceApiKeyPlaceholder": "输入您的 IO Intelligence API 密钥", "getIoIntelligenceApiKey": "获取 IO Intelligence API 密钥", @@ -377,6 +379,10 @@ "description": "不需要 API 密钥,但用户需要帮助将信息复制并粘贴到网页聊天 AI。", "instructions": "使用期间,将弹出对话框并自动将当前消息复制到剪贴板。您需要将这些内容粘贴到 AI 的网页版本(如 ChatGPT 或 Claude),然后将 AI 的回复复制回对话框并点击确认按钮。" }, + "roo": { + "authenticatedMessage": "已通过 Roo Code Cloud 账户安全认证。", + "connectButton": "连接到 Roo Code Cloud" + }, "openRouter": { "providerRouting": { "title": "OpenRouter 提供商路由", diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 0386a7540c..bf5c6fa4df 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -274,6 +274,12 @@ }, "announcement": { "title": "🎉 Roo Code {{version}} 已發布", + "stealthModel": { + "feature": "限時免費隱形模型 - 一個在代理程式編程方面表現出色的超快推理模型,擁有 262k 上下文視窗,透過 Roo Code Cloud 提供。", + "note": "(注意:提示和完成會被模型創建者記錄並用於改進模型)", + "connectButton": "連接到 Roo Code Cloud", + "selectModel": "在
設定中從 Roo Code Cloud 提供商選擇 roo/sonic 開始使用" + }, "description": "Roo Code {{version}} 帶來強大的新功能和重大改進,提升您的開發工作流程。", "whatsNew": "新功能", "feature1": "訊息佇列:在 Roo 工作時將多個訊息排入佇列,讓您可以不間斷地繼續規劃工作流程。", @@ -352,9 +358,9 @@ "ariaLabel": "版本 {{version}} - 點選查看發布說明" }, "rooCloudCTA": { - "title": "Roo Code Cloud 即將推出!", + "title": "Roo Code Cloud 正在進化!", "description": "在雲端執行 Roomote 遠端代理、隨時隨地存取您的工作、與他人協作等等。", - "joinWaitlist": "加入等候名單以獲得早期存取權限。" + "joinWaitlist": "註冊以獲得最新更新。" }, "command": { "triggerDescription": "觸發 {{name}} 命令" diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 6a673de60a..6bfab1435c 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -269,6 +269,8 @@ "getChutesApiKey": "取得 Chutes API 金鑰", "fireworksApiKey": "Fireworks API 金鑰", "getFireworksApiKey": "取得 Fireworks API 金鑰", + "featherlessApiKey": "Featherless API 金鑰", + "getFeatherlessApiKey": "取得 Featherless API 金鑰", "ioIntelligenceApiKey": "IO Intelligence API 金鑰", "ioIntelligenceApiKeyPlaceholder": "輸入您的 IO Intelligence API 金鑰", "getIoIntelligenceApiKey": "取得 IO Intelligence API 金鑰", @@ -377,6 +379,10 @@ "description": "不需要 API 金鑰,但使用者需要協助將資訊複製並貼上到網頁聊天 AI。", "instructions": "使用期間會彈出對話框,並自動將目前訊息複製到剪貼簿。您需要將這些內容貼上到網頁版 AI(如 ChatGPT 或 Claude),然後將 AI 的回覆複製回對話框並點選確認按鈕。" }, + "roo": { + "authenticatedMessage": "已透過 Roo Code Cloud 帳戶安全認證。", + "connectButton": "連接到 Roo Code Cloud" + }, "openRouter": { "providerRouting": { "title": "OpenRouter 供應商路由", diff --git a/webview-ui/src/utils/validate.ts b/webview-ui/src/utils/validate.ts index e4c9ed483d..348a373059 100644 --- a/webview-ui/src/utils/validate.ts +++ b/webview-ui/src/utils/validate.ts @@ -126,6 +126,11 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri return i18next.t("settings:validation.apiKey") } break + case "featherless": + if (!apiConfiguration.featherlessApiKey) { + return i18next.t("settings:validation.apiKey") + } + break } return undefined