diff --git a/packages/types/src/experiment.ts b/packages/types/src/experiment.ts index d7eb0b03d6..f299da519d 100644 --- a/packages/types/src/experiment.ts +++ b/packages/types/src/experiment.ts @@ -6,7 +6,13 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js" * ExperimentId */ -export const experimentIds = ["preventFocusDisruption", "imageGeneration", "runSlashCommand", "customTools"] as const +export const experimentIds = [ + "preventFocusDisruption", + "imageGeneration", + "runSlashCommand", + "customTools", + "subagent", +] as const export const experimentIdsSchema = z.enum(experimentIds) @@ -21,6 +27,7 @@ export const experimentsSchema = z.object({ imageGeneration: z.boolean().optional(), runSlashCommand: z.boolean().optional(), customTools: z.boolean().optional(), + subagent: z.boolean().optional(), }) export type Experiments = z.infer diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index e518972a1c..7c8c17cfb5 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -183,6 +183,7 @@ export type ClineSay = z.infer export const toolProgressStatusSchema = z.object({ icon: z.string().optional(), text: z.string().optional(), + spin: z.boolean().optional(), }) export type ToolProgressStatus = z.infer diff --git a/packages/types/src/tool.ts b/packages/types/src/tool.ts index 4f90b63e9f..638c8cd818 100644 --- a/packages/types/src/tool.ts +++ b/packages/types/src/tool.ts @@ -46,6 +46,7 @@ export const toolNames = [ "skill", "generate_image", "custom_tool", + "subagent", ] as const export const toolNamesSchema = z.enum(toolNames) diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index b20539afe4..546fd2b79c 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -783,6 +783,8 @@ export interface ClineSayTool { | "runSlashCommand" | "updateTodoList" | "skill" + | "subagentRunning" + | "subagentCompleted" path?: string // For readCommandOutput readStart?: number @@ -840,6 +842,13 @@ export interface ClineSayTool { description?: string // Properties for skill tool skill?: string + // Properties for subagent tool (subagentRunning / subagentCompleted) + currentTask?: string + result?: string + error?: string + /** When set (e.g. CANCELLED), webview shows t(messageKey) instead of result/error. */ + resultCode?: string + messageKey?: string } export interface ClineAskUseMcpServer { diff --git a/src/core/assistant-message/NativeToolCallParser.ts b/src/core/assistant-message/NativeToolCallParser.ts index bda7c71eb8..c9b5adae83 100644 --- a/src/core/assistant-message/NativeToolCallParser.ts +++ b/src/core/assistant-message/NativeToolCallParser.ts @@ -637,6 +637,20 @@ export class NativeToolCallParser { } break + case "subagent": + if ( + partialArgs.description !== undefined || + partialArgs.prompt !== undefined || + partialArgs.subagent_type !== undefined + ) { + nativeArgs = { + description: partialArgs.description, + prompt: partialArgs.prompt, + subagent_type: partialArgs.subagent_type, + } + } + break + default: break } @@ -911,6 +925,21 @@ export class NativeToolCallParser { } break + case "subagent": + if ( + args.description !== undefined && + args.prompt !== undefined && + args.subagent_type !== undefined && + (args.subagent_type === "general" || args.subagent_type === "explore") + ) { + nativeArgs = { + description: args.description, + prompt: args.prompt, + subagent_type: args.subagent_type, + } as NativeArgsFor + } + break + case "use_mcp_tool": if (args.server_name !== undefined && args.tool_name !== undefined) { nativeArgs = { diff --git a/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts b/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts index 2c15e12069..534742dfd2 100644 --- a/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts +++ b/src/core/assistant-message/__tests__/NativeToolCallParser.spec.ts @@ -291,6 +291,56 @@ describe("NativeToolCallParser", () => { }) }) }) + + describe("subagent tool", () => { + it("should parse description, prompt, and subagent_type into nativeArgs", () => { + const toolCall = { + id: "toolu_subagent_1", + name: "subagent" as const, + arguments: JSON.stringify({ + description: "Explore codebase", + prompt: "List all exports from src/index.ts", + subagent_type: "explore", + }), + } + + const result = NativeToolCallParser.parseToolCall(toolCall) + + expect(result).not.toBeNull() + expect(result?.type).toBe("tool_use") + if (result?.type === "tool_use") { + expect(result.nativeArgs).toBeDefined() + const nativeArgs = result.nativeArgs as { + description: string + prompt: string + subagent_type: "general" | "explore" + } + expect(nativeArgs.description).toBe("Explore codebase") + expect(nativeArgs.prompt).toBe("List all exports from src/index.ts") + expect(nativeArgs.subagent_type).toBe("explore") + } + }) + + it("should parse general subagent_type", () => { + const toolCall = { + id: "toolu_subagent_2", + name: "subagent" as const, + arguments: JSON.stringify({ + description: "Fix bug", + prompt: "Fix the null check in utils.ts", + subagent_type: "general", + }), + } + + const result = NativeToolCallParser.parseToolCall(toolCall) + + expect(result).not.toBeNull() + if (result?.type === "tool_use" && result.nativeArgs) { + const nativeArgs = result.nativeArgs as { subagent_type: string } + expect(nativeArgs.subagent_type).toBe("general") + } + }) + }) }) describe("processStreamingChunk", () => { diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 7f5862be15..0e98e77ad9 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -8,6 +8,7 @@ import { customToolRegistry } from "@roo-code/core" import { t } from "../../i18n" +import { SUBAGENT_STATUS_THINKING } from "../../shared/subagent" import { defaultModeSlug, getModeBySlug } from "../../shared/modes" import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../shared/tools" @@ -30,6 +31,7 @@ import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool" import { switchModeTool } from "../tools/SwitchModeTool" import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/AttemptCompletionTool" import { newTaskTool } from "../tools/NewTaskTool" +import { subagentTool } from "../tools/SubagentTool" import { updateTodoListTool } from "../tools/UpdateTodoListTool" import { runSlashCommandTool } from "../tools/RunSlashCommandTool" import { skillTool } from "../tools/SkillTool" @@ -85,6 +87,7 @@ export async function presentAssistantMessage(cline: Task) { } let block: any + let blocksConsumed = 1 try { // Performance optimization: Use shallow copy instead of deep clone. // The block is used read-only throughout this function - we never mutate its properties. @@ -292,6 +295,7 @@ export async function presentAssistantMessage(cline: Task) { content = content.replace(/\s?<\/thinking>/g, "") } + cline.subagentProgressCallback?.(SUBAGENT_STATUS_THINKING) await cline.say("text", content, undefined, block.partial) break } @@ -383,6 +387,8 @@ export async function presentAssistantMessage(cline: Task) { return `[${block.name} for '${block.params.skill}'${block.params.args ? ` with args: ${block.params.args}` : ""}]` case "generate_image": return `[${block.name} for '${block.params.path}']` + case "subagent": + return `[${block.name}: ${block.params.description ?? "(no description)"}]` default: return `[${block.name}]` } @@ -405,8 +411,36 @@ export async function presentAssistantMessage(cline: Task) { break } - // Track if we've already pushed a tool result for this tool call (native tool calling only) - let hasToolResult = false + // Track which tool_use_ids have already received a tool_result (native tool calling only) + const toolResultsPushed = new Set() + const pushToolResultFor = (id: string, content: ToolResponse) => { + if (toolResultsPushed.has(id)) { + console.warn(`[presentAssistantMessage] Skipping duplicate tool_result for tool_use_id: ${id}`) + return + } + let resultContent: string + const imageBlocks: Anthropic.ImageBlockParam[] = [] + if (typeof content === "string") { + resultContent = content || "(tool did not return anything)" + } else { + const textBlocks = content.filter((item) => item.type === "text") + imageBlocks.push( + ...(content.filter((item) => item.type === "image") as Anthropic.ImageBlockParam[]), + ) + resultContent = + textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") || + "(tool did not return anything)" + } + cline.pushToolResultToUserContent({ + type: "tool_result", + tool_use_id: sanitizeToolUseId(id), + content: resultContent, + }) + if (imageBlocks.length > 0) { + cline.userMessageContent.push(...imageBlocks) + } + toolResultsPushed.add(id) + } // If this is a native tool call but the parser couldn't construct nativeArgs // (e.g., malformed/unfinished JSON in a streaming tool call), we must NOT attempt to @@ -447,17 +481,8 @@ export async function presentAssistantMessage(cline: Task) { let approvalFeedback: { text: string; images?: string[] } | undefined const pushToolResult = (content: ToolResponse) => { - // Native tool calling: only allow ONE tool_result per tool call - if (hasToolResult) { - console.warn( - `[presentAssistantMessage] Skipping duplicate tool_result for tool_use_id: ${toolCallId}`, - ) - return - } - let resultContent: string let imageBlocks: Anthropic.ImageBlockParam[] = [] - if (typeof content === "string") { resultContent = content || "(tool did not return anything)" } else { @@ -467,7 +492,6 @@ export async function presentAssistantMessage(cline: Task) { textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") || "(tool did not return anything)" } - // Merge approval feedback into tool result (GitHub #10465) if (approvalFeedback) { const feedbackText = formatResponse.toolApprovedWithFeedback(approvalFeedback.text) @@ -477,18 +501,9 @@ export async function presentAssistantMessage(cline: Task) { imageBlocks = [...feedbackImageBlocks, ...imageBlocks] } } - - cline.pushToolResultToUserContent({ - type: "tool_result", - tool_use_id: sanitizeToolUseId(toolCallId), - content: resultContent, - }) - - if (imageBlocks.length > 0) { - cline.userMessageContent.push(...imageBlocks) - } - - hasToolResult = true + const mergedContent: ToolResponse = + imageBlocks.length > 0 ? [{ type: "text", text: resultContent }, ...imageBlocks] : resultContent + pushToolResultFor(toolCallId, mergedContent) } const askApproval = async ( @@ -675,6 +690,7 @@ export async function presentAssistantMessage(cline: Task) { } } + cline.subagentProgressCallback?.(toolDescription()) switch (block.name) { case "write_to_file": await checkpointSaveAndMark(cline) @@ -812,6 +828,92 @@ export async function presentAssistantMessage(cline: Task) { toolCallId: block.id, }) break + case "subagent": { + if (block.partial) { + break + } + // Collect consecutive subagent tool_use blocks for potential parallel execution + const subagentBlocks: (typeof block)[] = [block] + for ( + let i = cline.currentStreamingContentIndex + 1; + i < cline.assistantMessageContent.length; + i++ + ) { + const b = cline.assistantMessageContent[i] + if (b?.type === "tool_use" && b?.name === "subagent" && !b.partial) { + subagentBlocks.push(b) + } else { + break + } + } + if (subagentBlocks.length === 1) { + await subagentTool.handle(cline, block as ToolUse<"subagent">, { + askApproval, + handleError, + pushToolResult, + toolCallId: block.id, + }) + } else { + // Parallel subagent batch: start all, await all, finish each + const subagentCallbacks = (subagentBlock: typeof block) => ({ + askApproval, + handleError, + pushToolResult: (content: ToolResponse) => pushToolResultFor(subagentBlock.id, content), + toolCallId: subagentBlock.id, + }) + const getParams = ( + b: typeof block, + ): { description: string; prompt: string; subagent_type: "general" | "explore" } => { + if (b.nativeArgs && typeof b.nativeArgs === "object") { + const a = b.nativeArgs as Record + return { + description: a.description ?? "", + prompt: a.prompt ?? "", + subagent_type: (a.subagent_type === "explore" ? "explore" : "general") as + | "general" + | "explore", + } + } + return subagentTool.parseLegacy((b.params ?? {}) as Record) + } + const promises = subagentBlocks.map((subagentBlock) => + subagentTool.startAndReturnPromise( + cline, + getParams(subagentBlock), + subagentCallbacks(subagentBlock), + ), + ) + const results = await Promise.allSettled(promises) + for (let i = 0; i < subagentBlocks.length; i++) { + const subagentBlock = subagentBlocks[i] + const settled = results[i] + if (settled?.status === "fulfilled") { + await subagentTool.finish( + cline, + getParams(subagentBlock), + settled.value, + subagentCallbacks(subagentBlock), + ) + } + if (settled?.status === "rejected") { + cline.finalizeSubagentRunning(subagentBlock.id) + // startAndReturnPromise already pushed error for missing params; other errors need a tool_result + if ( + settled.reason?.message !== "Missing description" && + settled.reason?.message !== "Missing prompt" && + settled.reason?.message !== "Provider reference lost" + ) { + pushToolResultFor( + subagentBlock.id, + formatResponse.toolError("The subagent failed."), + ) + } + } + } + blocksConsumed = subagentBlocks.length + } + break + } case "attempt_completion": { const completionCallbacks: AttemptCompletionCallbacks = { askApproval, @@ -939,7 +1041,7 @@ export async function presentAssistantMessage(cline: Task) { // (instead of preemptively doing it in iterator). if (!block.partial || cline.didRejectTool || cline.didAlreadyUseTool) { // Block is finished streaming and executing. - if (cline.currentStreamingContentIndex === cline.assistantMessageContent.length - 1) { + if (cline.currentStreamingContentIndex + blocksConsumed >= cline.assistantMessageContent.length) { // It's okay that we increment if !didCompleteReadingStream, it'll // just return because out of bounds and as streaming continues it // will call `presentAssitantMessage` if a new block is ready. If @@ -952,9 +1054,8 @@ export async function presentAssistantMessage(cline: Task) { // Call next block if it exists (if not then read stream will call it // when it's ready). - // Need to increment regardless, so when read stream calls this function - // again it will be streaming the next block. - cline.currentStreamingContentIndex++ + // Need to increment by blocksConsumed (e.g. 1 normally, or N when we processed a parallel subagent batch). + cline.currentStreamingContentIndex += blocksConsumed if (cline.currentStreamingContentIndex < cline.assistantMessageContent.length) { // There are already more content blocks to stream, so we'll call diff --git a/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts b/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts index 0b776a2bad..78d51b9957 100644 --- a/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts +++ b/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts @@ -89,3 +89,55 @@ describe("filterNativeToolsForMode - disabledTools", () => { expect(resultNames).not.toContain("edit") }) }) + +describe("filterNativeToolsForMode - subagent experiment", () => { + const codeMode = { + slug: "code", + name: "Code", + roleDefinition: "Test", + groups: ["read", "edit", "command", "mcp"] as ("read" | "edit" | "command" | "mcp")[], + } + + const mockSubagentTool: OpenAI.Chat.ChatCompletionTool = { + type: "function", + function: { + name: "subagent", + description: "Run subagent", + parameters: {}, + }, + } + + const mockNativeTools: OpenAI.Chat.ChatCompletionTool[] = [ + makeTool("read_file"), + makeTool("write_to_file"), + mockSubagentTool, + ] + + it("should exclude subagent when experiment is not enabled", () => { + const filtered = filterNativeToolsForMode( + mockNativeTools, + "code", + [codeMode], + { subagent: false }, + undefined, + {}, + undefined, + ) + const toolNames = filtered.map((t) => ("function" in t ? t.function.name : "")) + expect(toolNames).not.toContain("subagent") + }) + + it("should include subagent when experiment is enabled", () => { + const filtered = filterNativeToolsForMode( + mockNativeTools, + "code", + [codeMode], + { subagent: true }, + undefined, + {}, + undefined, + ) + const toolNames = filtered.map((t) => ("function" in t ? t.function.name : "")) + expect(toolNames).toContain("subagent") + }) +}) diff --git a/src/core/prompts/tools/filter-tools-for-mode.ts b/src/core/prompts/tools/filter-tools-for-mode.ts index fdd41e7e33..c90a9022a6 100644 --- a/src/core/prompts/tools/filter-tools-for-mode.ts +++ b/src/core/prompts/tools/filter-tools-for-mode.ts @@ -291,6 +291,10 @@ export function filterNativeToolsForMode( allowedToolNames.delete("run_slash_command") } + if (!experiments?.subagent) { + allowedToolNames.delete("subagent") + } + // Remove tools that are explicitly disabled via the disabledTools setting if (settings?.disabledTools?.length) { for (const toolName of settings.disabledTools) { @@ -379,6 +383,9 @@ export function isToolAllowedInMode( if (toolName === "run_slash_command") { return experiments?.runSlashCommand === true } + if (toolName === "subagent") { + return experiments?.subagent === true + } return true } diff --git a/src/core/prompts/tools/native-tools/index.ts b/src/core/prompts/tools/native-tools/index.ts index 758914d2d6..3488631dd5 100644 --- a/src/core/prompts/tools/native-tools/index.ts +++ b/src/core/prompts/tools/native-tools/index.ts @@ -13,6 +13,7 @@ import newTask from "./new_task" import readCommandOutput from "./read_command_output" import { createReadFileTool, type ReadFileToolOptions } from "./read_file" import runSlashCommand from "./run_slash_command" +import subagent from "./subagent" import skill from "./skill" import searchReplace from "./search_replace" import edit_file from "./edit_file" @@ -60,6 +61,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch readCommandOutput, createReadFileTool(readFileOptions), runSlashCommand, + subagent, skill, searchReplace, edit_file, diff --git a/src/core/prompts/tools/native-tools/subagent.ts b/src/core/prompts/tools/native-tools/subagent.ts new file mode 100644 index 0000000000..36fa48e77a --- /dev/null +++ b/src/core/prompts/tools/native-tools/subagent.ts @@ -0,0 +1,36 @@ +import type OpenAI from "openai" + +const SUBAGENT_DESCRIPTION = `Run a subagent in the background to do a focused sub-task. When the subagent finishes, its result or summary is returned as this tool's result and you can use it in your next step. Use when: (1) you need to offload research, exploration, or a multi-step sub-task without switching the user's view, or (2) you want read-only exploration (subagent_type "explore") with no file edits or commands. Do not use for creating a new user-visible task—use new_task instead.` + +const DESCRIPTION_PARAM = `Short label for this subagent, shown in the chat (e.g. "List exports in src", "Check README")` +const PROMPT_PARAM = `Full instructions for the subagent. Its result or summary will be returned as this tool's result.` +const SUBAGENT_TYPE_PARAM = `"explore": read-only—subagent can only use read/search/list tools (no file edits or commands). Use for research or gathering information. "general": full tools—subagent can read, edit, and run commands. Use when the sub-task may need to change files or run commands.` + +export default { + type: "function", + function: { + name: "subagent", + description: SUBAGENT_DESCRIPTION, + strict: true, + parameters: { + type: "object", + properties: { + description: { + type: "string", + description: DESCRIPTION_PARAM, + }, + prompt: { + type: "string", + description: PROMPT_PARAM, + }, + subagent_type: { + type: "string", + description: SUBAGENT_TYPE_PARAM, + enum: ["general", "explore"], + }, + }, + required: ["description", "prompt", "subagent_type"], + additionalProperties: false, + }, + }, +} satisfies OpenAI.Chat.ChatCompletionTool diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 005bb0f292..a83790f7f7 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -70,6 +70,7 @@ import { t } from "../../i18n" import { getApiMetrics, hasTokenUsageChanged, hasToolUsageChanged } from "../../shared/getApiMetrics" import { ClineAskResponse } from "../../shared/WebviewMessage" import { defaultModeSlug, getModeBySlug } from "../../shared/modes" +import { SUBAGENT_TOOL_NAMES, type SubagentRunningPayload, type SubagentStructuredResult } from "../../shared/subagent" import { DiffStrategy, type ToolUse, type ToolParamName, toolParamNames } from "../../shared/tools" import { getModelMaxOutputTokens } from "../../shared/api" @@ -157,6 +158,10 @@ export interface TaskOptions extends CreateTaskOptions { workspacePath?: string /** Initial status for the task's history item (e.g., "active" for child tasks) */ initialStatus?: "active" | "delegated" | "completed" + /** When set, this task runs as a background subagent; "explore" = read-only tools */ + subagentType?: "general" | "explore" + /** When false, task is not persisted to task history (e.g. subagents). Defaults to false when subagentType is set, true otherwise. */ + needUpdateHistory?: boolean } export class Task extends EventEmitter implements TaskLike { @@ -417,6 +422,17 @@ export class Task extends EventEmitter implements TaskLike { // MessageManager for high-level message operations (lazy initialized) private _messageManager?: MessageManager + /** When set, attempt_completion will resolve this and abort instead of normal flow */ + public backgroundCompletionResolve?: (result: string | SubagentStructuredResult) => void + /** When set, tool building is restricted to read-only for "explore" */ + public subagentType?: "general" | "explore" + /** When set, child reports current step (e.g. tool description) for parent to show in subagentRunning row */ + public subagentProgressCallback?: (currentTask: string) => void + /** When false, saveClineMessages does not call updateTaskHistory (e.g. subagents). */ + private readonly needUpdateHistory: boolean + /** When this task is the parent of running subagents, holds child tasks keyed by toolCallId until they complete or are cancelled. */ + public activeSubagentChildren = new Map() + constructor({ provider, apiConfiguration, @@ -436,9 +452,14 @@ export class Task extends EventEmitter implements TaskLike { initialTodos, workspacePath, initialStatus, + subagentType, + needUpdateHistory, }: TaskOptions) { super() + this.subagentType = subagentType + this.needUpdateHistory = needUpdateHistory ?? subagentType === undefined + if (startTask && !task && !images && !historyItem) { throw new Error("Either historyItem or task/images must be provided") } @@ -1240,7 +1261,9 @@ export class Task extends EventEmitter implements TaskLike { // - Final state is emitted when updates stop (trailing: true) this.debouncedEmitTokenUsage(tokenUsage, this.toolUsage) - await this.providerRef.deref()?.updateTaskHistory(historyItem) + if (this.needUpdateHistory) { + await this.providerRef.deref()?.updateTaskHistory(historyItem) + } return true } catch (error) { console.error("Failed to save Roo messages:", error) @@ -1258,6 +1281,51 @@ export class Task extends EventEmitter implements TaskLike { return undefined } + /** + * Updates a "subagentRunning" say message with currentTask so the UI can show it in real time. + * When runId is set, updates the last message whose payload has that runId (for parallel subagents). + * When runId is not set, updates the last subagentRunning message (backward compatibility). + * No-op if no matching message exists. + */ + public reportSubagentProgress(currentTask: string, runId?: string): void { + const idx = this.findSubagentRunningIndex(runId) + if (idx === -1) return + const msg = this.clineMessages[idx] + try { + const payload = JSON.parse(msg.text!) as SubagentRunningPayload + payload.currentTask = currentTask + msg.text = JSON.stringify(payload) + void this.updateClineMessage(msg) + } catch { + // ignore malformed message + } + } + + /** + * Removes a "subagentRunning" message entirely so only the completed message remains. + * Called when the subagent finishes (success or error). + */ + public finalizeSubagentRunning(runId?: string): void { + const idx = this.findSubagentRunningIndex(runId) + if (idx === -1) return + this.clineMessages.splice(idx, 1) + void this.saveClineMessages() + } + + private findSubagentRunningIndex(runId?: string): number { + return findLastIndex(this.clineMessages, (m) => { + if (m.type !== "say" || m.say !== "tool" || !m.text) return false + try { + const parsed = JSON.parse(m.text) as SubagentRunningPayload + if (parsed.tool !== SUBAGENT_TOOL_NAMES.running) return false + if (runId !== undefined) return parsed.runId === runId + return true + } catch { + return false + } + }) + } + // Note that `partial` has three valid states true (partial message), // false (completion of partial message), undefined (individual complete // message). @@ -1674,6 +1742,7 @@ export class Task extends EventEmitter implements TaskLike { disabledTools: state?.disabledTools, modelInfo, includeAllToolsWithRestrictions: false, + subagentType: this.subagentType, }) allTools = toolsResult.tools } @@ -1798,6 +1867,7 @@ export class Task extends EventEmitter implements TaskLike { text, images, partial, + progressStatus, contextCondense, contextTruncation, }) @@ -1836,6 +1906,7 @@ export class Task extends EventEmitter implements TaskLike { say: type, text, images, + progressStatus, contextCondense, contextTruncation, }) @@ -1860,6 +1931,7 @@ export class Task extends EventEmitter implements TaskLike { text, images, checkpoint, + progressStatus, contextCondense, contextTruncation, }) @@ -1876,6 +1948,28 @@ export class Task extends EventEmitter implements TaskLike { return formatResponse.toolError(formatResponse.missingToolParameterError(paramName)) } + /** + * Starts the task loop with the given prompt. Used for background subagents. + * Call only when task was created with startTask: false and subagentType set. + */ + public async runBackgroundSubagentLoop(initialPrompt: string): Promise { + this.clineMessages = [] + this.apiConversationHistory = [] + const typeInstructions = + this.subagentType === "explore" + ? "You are running as an **explore** subagent (read-only). Use only read, search, and list tools; do not edit files or run commands. Gather the requested information and put your findings and summary in your completion; that will be returned to the parent task.\n\n" + : "You are running as a **general** subagent with full tool access. You may read, edit files, and run commands as needed. Put your findings and final result in your completion; that summary will be returned to the parent task.\n\n" + const taskContent = `${typeInstructions}${initialPrompt}` + await this.say("text", taskContent) + this.isInitialized = true + await this.initiateTaskLoop([{ type: "text", text: `\n${taskContent}\n` }]).catch((error) => { + if (this.abandoned === true || this.backgroundCompletionResolve === undefined) { + return + } + throw error + }) + } + // Lifecycle // Start / Resume / Abort / Dispose @@ -3866,6 +3960,7 @@ export class Task extends EventEmitter implements TaskLike { disabledTools: state?.disabledTools, modelInfo, includeAllToolsWithRestrictions: false, + subagentType: this.subagentType, }) allTools = toolsResult.tools } @@ -4080,6 +4175,7 @@ export class Task extends EventEmitter implements TaskLike { disabledTools: state?.disabledTools, modelInfo, includeAllToolsWithRestrictions: false, + subagentType: this.subagentType, }) contextMgmtTools = toolsResult.tools } @@ -4244,6 +4340,7 @@ export class Task extends EventEmitter implements TaskLike { disabledTools: state?.disabledTools, modelInfo, includeAllToolsWithRestrictions: supportsAllowedFunctionNames, + subagentType: this.subagentType, }) allTools = toolsResult.tools allowedFunctionNames = toolsResult.allowedFunctionNames diff --git a/src/core/task/__tests__/flushPendingToolResultsToHistory.spec.ts b/src/core/task/__tests__/flushPendingToolResultsToHistory.spec.ts index f19645d969..cf8bd558a5 100644 --- a/src/core/task/__tests__/flushPendingToolResultsToHistory.spec.ts +++ b/src/core/task/__tests__/flushPendingToolResultsToHistory.spec.ts @@ -448,3 +448,164 @@ describe("flushPendingToolResultsToHistory", () => { expect(task.apiConversationHistory.length).toBe(0) }) }) + +describe("reportSubagentProgress", () => { + let mockProvider: any + let mockApiConfig: ProviderSettings + let mockExtensionContext: vscode.ExtensionContext + + beforeEach(() => { + if (!TelemetryService.hasInstance()) { + TelemetryService.createInstance([]) + } + const storageUri = { fsPath: path.join(os.tmpdir(), "test-storage-reportSubagent") } + mockExtensionContext = { + globalState: { + get: vi.fn().mockImplementation((_key: keyof GlobalState) => undefined), + update: vi.fn().mockImplementation((_k, _v) => Promise.resolve()), + keys: vi.fn().mockReturnValue([]), + }, + globalStorageUri: storageUri, + workspaceState: { + get: vi.fn().mockImplementation((_k) => undefined), + update: vi.fn().mockImplementation((_k, _v) => Promise.resolve()), + keys: vi.fn().mockReturnValue([]), + }, + secrets: { + get: vi.fn().mockResolvedValue(undefined), + store: vi.fn().mockResolvedValue(undefined), + delete: vi.fn().mockResolvedValue(undefined), + }, + extensionUri: { fsPath: "/mock/extension" }, + extension: { packageJSON: { version: "1.0.0" } }, + } as unknown as vscode.ExtensionContext + const mockOutputChannel = { + name: "test", + appendLine: vi.fn(), + append: vi.fn(), + clear: vi.fn(), + show: vi.fn(), + hide: vi.fn(), + dispose: vi.fn(), + replace: vi.fn(), + } + mockProvider = new ClineProvider( + mockExtensionContext, + mockOutputChannel, + "sidebar", + new ContextProxy(mockExtensionContext), + ) as any + mockApiConfig = { + apiProvider: "anthropic", + apiModelId: "claude-3-5-sonnet-20241022", + apiKey: "test-key", + } + mockProvider.postMessageToWebview = vi.fn().mockResolvedValue(undefined) + mockProvider.postStateToWebview = vi.fn().mockResolvedValue(undefined) + mockProvider.updateTaskHistory = vi.fn().mockResolvedValue(undefined) + }) + + it("updates last subagentRunning message with currentTask and calls postMessageToWebview", () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "parent task", + startTask: false, + }) + const ts = 12345 + task.clineMessages = [ + { + type: "say", + say: "tool", + text: JSON.stringify({ tool: "subagentRunning", description: "Explore codebase" }), + ts, + }, + ] + + task.reportSubagentProgress("Read file for src/index.ts") + + const updated = JSON.parse(task.clineMessages[0].text!) + expect(updated.currentTask).toBe("Read file for src/index.ts") + expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "messageUpdated", + clineMessage: task.clineMessages[0], + }) + }) + + it("does nothing when no subagentRunning message exists", () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "parent task", + startTask: false, + }) + task.clineMessages = [{ type: "say", say: "tool", text: JSON.stringify({ tool: "runSlashCommand" }), ts: 1 }] + + task.reportSubagentProgress("Step one") + + expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled() + expect(task.clineMessages[0].text).not.toContain("currentTask") + }) + + it("updates the last subagentRunning when multiple tool messages exist", () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "parent task", + startTask: false, + }) + task.clineMessages = [ + { type: "say", say: "tool", text: JSON.stringify({ tool: "readFile" }), ts: 1 }, + { + type: "say", + say: "tool", + text: JSON.stringify({ tool: "subagentRunning", description: "Sub" }), + ts: 2, + }, + ] + + task.reportSubagentProgress("Attempt completion...") + + const lastToolMsg = task.clineMessages[1] + const parsed = JSON.parse(lastToolMsg.text!) + expect(parsed.currentTask).toBe("Attempt completion...") + expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "messageUpdated", + clineMessage: lastToolMsg, + }) + }) + + it("updates only the subagentRunning message with matching runId when runId is provided", () => { + const task = new Task({ + provider: mockProvider, + apiConfiguration: mockApiConfig, + task: "parent task", + startTask: false, + }) + task.clineMessages = [ + { + type: "say", + say: "tool", + text: JSON.stringify({ tool: "subagentRunning", description: "Sub A", runId: "id-a" }), + ts: 1, + }, + { + type: "say", + say: "tool", + text: JSON.stringify({ tool: "subagentRunning", description: "Sub B", runId: "id-b" }), + ts: 2, + }, + ] + + task.reportSubagentProgress("Progress for B only", "id-b") + + const msgA = JSON.parse(task.clineMessages[0].text!) + const msgB = JSON.parse(task.clineMessages[1].text!) + expect(msgA.currentTask).toBeUndefined() + expect(msgB.currentTask).toBe("Progress for B only") + expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({ + type: "messageUpdated", + clineMessage: task.clineMessages[1], + }) + }) +}) diff --git a/src/core/task/build-tools.ts b/src/core/task/build-tools.ts index c32d8f6f9b..5be3862b34 100644 --- a/src/core/task/build-tools.ts +++ b/src/core/task/build-tools.ts @@ -2,7 +2,7 @@ import path from "path" import type OpenAI from "openai" -import type { ProviderSettings, ModeConfig, ModelInfo } from "@roo-code/types" +import type { ProviderSettings, ModeConfig, ModelInfo, ToolName } from "@roo-code/types" import { customToolRegistry, formatNative } from "@roo-code/core" import type { ClineProvider } from "../webview/ClineProvider" @@ -14,6 +14,42 @@ import { filterMcpToolsForMode, resolveToolAlias, } from "../prompts/tools/filter-tools-for-mode" +import type { SubagentType } from "../../shared/subagent" +import { TOOL_GROUPS } from "../../shared/tools" + +/** Tools that require user interaction or affect parent-level UI. Disallowed for all subagents (general and explore). */ +const SUBAGENT_EXCLUDE_TOOLS: Set = new Set([ + "ask_followup_question", + "new_task", + "switch_mode", + "update_todo_list", +] as ToolName[]) + +/** + * Applies subagent-specific tool restrictions: for "explore" only read-only tools + attempt_completion; + * for any subagent, excludes nested subagent and tools that require user interaction. + */ +function applySubagentToolRestrictions( + tools: T[], + subagentType: SubagentType | undefined, + getName: (t: T) => string, + resolveName: (name: string) => string, +): T[] { + let result = tools + if (subagentType === "explore") { + const readOnlyNames = new Set([...TOOL_GROUPS.read.tools, "attempt_completion"]) + result = result.filter((t) => readOnlyNames.has(resolveName(getName(t)))) + } + if (subagentType !== undefined) { + result = result.filter((t) => { + const name = resolveName(getName(t)) + if (name === "subagent") return false + if (SUBAGENT_EXCLUDE_TOOLS.has(name)) return false + return true + }) + } + return result +} interface BuildToolsOptions { provider: ClineProvider @@ -31,6 +67,8 @@ interface BuildToolsOptions { * to pass all tool definitions while restricting callable tools. */ includeAllToolsWithRestrictions?: boolean + /** When "explore", only read-only tools are allowed (no edit/command) */ + subagentType?: "general" | "explore" } interface BuildToolsResult { @@ -90,6 +128,7 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO disabledTools, modelInfo, includeAllToolsWithRestrictions, + subagentType, } = options const mcpHub = provider.getMcpHub() @@ -141,8 +180,9 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO } } - // Combine filtered tools (for backward compatibility and for allowedFunctionNames) - const filteredTools = [...filteredNativeTools, ...filteredMcpTools, ...nativeCustomTools] + // Combine filtered tools, then apply subagent restrictions when this task is a subagent + const combined = [...filteredNativeTools, ...filteredMcpTools, ...nativeCustomTools] + const filteredTools = applySubagentToolRestrictions(combined, subagentType, getToolName, resolveToolAlias) // If includeAllToolsWithRestrictions is true, return ALL tools but provide // allowed names based on mode filtering @@ -150,7 +190,7 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO // Combine ALL tools (unfiltered native + all MCP + custom) const allTools = [...nativeTools, ...mcpTools, ...nativeCustomTools] - // Extract names of tools that are allowed based on mode filtering. + // Extract names of tools that are allowed based on mode filtering and subagent restrictions. // Resolve any alias names to canonical names to ensure consistency with allTools // (which uses canonical names). This prevents Gemini errors when tools are renamed // to aliases in filteredTools but allTools contains the original canonical names. diff --git a/src/core/tools/AttemptCompletionTool.ts b/src/core/tools/AttemptCompletionTool.ts index a70576d75f..82ba6b10ac 100644 --- a/src/core/tools/AttemptCompletionTool.ts +++ b/src/core/tools/AttemptCompletionTool.ts @@ -80,6 +80,19 @@ export class AttemptCompletionTool extends BaseTool<"attempt_completion"> { await task.say("completion_result", result, undefined, false) + if (task.backgroundCompletionResolve) { + // Subagent path: emit TaskCompleted and telemetry once, then resolve and abort. + // Do not run this for normal tasks—emitTaskCompleted() runs on delegation (line 124) or user acceptance (line 151). + task.emitFinalTokenUsageUpdate() + TelemetryService.instance.captureTaskCompleted(task.taskId) + task.emit(RooCodeEventName.TaskCompleted, task.taskId, task.getTokenUsage(), task.toolUsage) + task.subagentProgressCallback = undefined + task.backgroundCompletionResolve(result) + task.backgroundCompletionResolve = undefined + task.abortTask() + return + } + // Check for subtask using parentTaskId (metadata-driven delegation) if (task.parentTaskId) { // Check if this subtask has already completed and returned to parent diff --git a/src/core/tools/SubagentTool.ts b/src/core/tools/SubagentTool.ts new file mode 100644 index 0000000000..be7e078635 --- /dev/null +++ b/src/core/tools/SubagentTool.ts @@ -0,0 +1,192 @@ +import { + SUBAGENT_CANCELLED_MODEL_MESSAGE, + SUBAGENT_FAILED_MODEL_MESSAGE, + SUBAGENT_STATUS_STARTING, + SUBAGENT_TOOL_NAMES, + type RunSubagentInBackgroundParams, + type SubagentCompletedPayload, + type SubagentRunningPayload, + type SubagentStructuredResult, + isSubagentRunner, +} from "../../shared/subagent" +import type { ToolUse } from "../../shared/tools" +import { Task } from "../task/Task" +import { formatResponse } from "../prompts/responses" +import { BaseTool, ToolCallbacks } from "./BaseTool" + +interface SubagentParams { + description: string + prompt: string + subagent_type: "general" | "explore" +} + +export class SubagentTool extends BaseTool<"subagent"> { + readonly name = "subagent" as const + + parseLegacy(params: Partial>): SubagentParams { + return { + description: params.description ?? "", + prompt: params.prompt ?? "", + subagent_type: (params.subagent_type === "general" || params.subagent_type === "explore" + ? params.subagent_type + : "general") as "general" | "explore", + } + } + + /** + * Starts a subagent and returns its Promise without awaiting. Used for parallel subagent batches. + * Caller must await the Promise then call finish() with the result. + */ + async startAndReturnPromise( + task: Task, + params: SubagentParams, + callbacks: ToolCallbacks, + ): Promise { + const { description, prompt, subagent_type } = params + const toolCallId = callbacks.toolCallId ?? `subagent-${Date.now()}-${Math.random().toString(36).slice(2)}` + + const provider = task.providerRef.deref() + if (!provider || !isSubagentRunner(provider)) { + callbacks.pushToolResult(formatResponse.toolError("Provider reference lost")) + return Promise.reject(new Error("Provider reference lost")) + } + + if (!description?.trim()) { + task.consecutiveMistakeCount++ + task.recordToolError("subagent") + task.didToolFailInCurrentTurn = true + task.sayAndCreateMissingParamError("subagent", "description").then((msg) => callbacks.pushToolResult(msg)) + return Promise.reject(new Error("Missing description")) + } + if (!prompt?.trim()) { + task.consecutiveMistakeCount++ + task.recordToolError("subagent") + task.didToolFailInCurrentTurn = true + task.sayAndCreateMissingParamError("subagent", "prompt").then((msg) => callbacks.pushToolResult(msg)) + return Promise.reject(new Error("Missing prompt")) + } + + task.consecutiveMistakeCount = 0 + + const runningPayload: SubagentRunningPayload = { + tool: SUBAGENT_TOOL_NAMES.running, + description, + currentTask: SUBAGENT_STATUS_STARTING, + runId: toolCallId, + } + const runningText = JSON.stringify(runningPayload) + const progressStatus = { icon: "sync", spin: true } + + await task.say("tool", runningText, undefined, undefined, undefined, progressStatus, { + isNonInteractive: true, + }) + + const runParams: RunSubagentInBackgroundParams = { + parentTaskId: task.taskId, + prompt, + subagentType: subagent_type, + onProgress: (currentTask) => task.reportSubagentProgress(currentTask, toolCallId), + toolCallId, + } + return provider.runSubagentInBackground(runParams) + } + + /** + * Emits subagentCompleted and pushes the tool result. Call after startAndReturnPromise's Promise resolves. + */ + async finish( + task: Task, + params: SubagentParams, + result: string | SubagentStructuredResult, + callbacks: ToolCallbacks, + ): Promise { + const { description } = params + const { pushToolResult } = callbacks + const toolCallId = callbacks.toolCallId + + const isStructured = (r: string | SubagentStructuredResult): r is SubagentStructuredResult => + typeof r === "object" && r !== null && "code" in r && "messageKey" in r + + // Stop the spinner on the "Running subagent" row + task.finalizeSubagentRunning(toolCallId) + + let completedPayload: SubagentCompletedPayload + let toolResult: string + if (isStructured(result)) { + completedPayload = { + tool: SUBAGENT_TOOL_NAMES.completed, + description, + result: SUBAGENT_CANCELLED_MODEL_MESSAGE, + resultCode: result.code, + messageKey: result.messageKey, + } + toolResult = SUBAGENT_CANCELLED_MODEL_MESSAGE + } else { + completedPayload = { + tool: SUBAGENT_TOOL_NAMES.completed, + description, + result, + } + toolResult = result + } + const completedText = JSON.stringify(completedPayload) + await task.say("tool", completedText, undefined, undefined, undefined, undefined, { + isNonInteractive: true, + }) + pushToolResult(formatResponse.toolResult(toolResult)) + } + + async execute(params: SubagentParams, task: Task, callbacks: ToolCallbacks): Promise { + const { pushToolResult } = callbacks + + const provider = task.providerRef.deref() + if (!provider || !isSubagentRunner(provider)) { + pushToolResult(formatResponse.toolError("Provider reference lost")) + return + } + + if (!params.description?.trim()) { + task.consecutiveMistakeCount++ + task.recordToolError("subagent") + task.didToolFailInCurrentTurn = true + pushToolResult(await task.sayAndCreateMissingParamError("subagent", "description")) + return + } + if (!params.prompt?.trim()) { + task.consecutiveMistakeCount++ + task.recordToolError("subagent") + task.didToolFailInCurrentTurn = true + pushToolResult(await task.sayAndCreateMissingParamError("subagent", "prompt")) + return + } + + try { + const result = await this.startAndReturnPromise(task, params, callbacks) + await this.finish(task, params, result, callbacks) + } catch (error) { + if ( + error instanceof Error && + (error.message === "Missing description" || + error.message === "Missing prompt" || + error.message === "Provider reference lost") + ) { + return + } + console.error("Subagent failed:", error) + task.finalizeSubagentRunning(callbacks.toolCallId) + task.recordToolError("subagent") + const errorPayload: SubagentCompletedPayload = { + tool: SUBAGENT_TOOL_NAMES.completed, + description: params.description, + error: SUBAGENT_FAILED_MODEL_MESSAGE, + } + const errorPayloadStr = JSON.stringify(errorPayload) + await task.say("tool", errorPayloadStr, undefined, undefined, undefined, undefined, { + isNonInteractive: true, + }) + pushToolResult(formatResponse.toolError(SUBAGENT_FAILED_MODEL_MESSAGE)) + } + } +} + +export const subagentTool = new SubagentTool() diff --git a/src/core/tools/__tests__/SubagentTool.spec.ts b/src/core/tools/__tests__/SubagentTool.spec.ts new file mode 100644 index 0000000000..7dacdea6e9 --- /dev/null +++ b/src/core/tools/__tests__/SubagentTool.spec.ts @@ -0,0 +1,241 @@ +import { + SUBAGENT_CANCELLED_MODEL_MESSAGE, + SUBAGENT_CANCELLED_STRUCTURED_RESULT, + SUBAGENT_FAILED_MODEL_MESSAGE, + type SubagentStructuredResult, +} from "../../../shared/subagent" +import { subagentTool } from "../SubagentTool" +import type { ToolUse } from "../../../shared/tools" + +vi.mock("../../prompts/responses", () => ({ + formatResponse: { + toolError: (msg: string) => `Tool Error: ${msg}`, + toolResult: (content: string) => content, + }, +})) + +const mockRunSubagentInBackground = + vi.fn< + (p: { + parentTaskId: string + prompt: string + subagentType: "general" | "explore" + toolCallId: string + }) => Promise + >() +const mockSay = vi.fn() +const mockPushToolResult = vi.fn() +const mockHandleError = vi.fn() +const mockSayAndCreateMissingParamError = vi.fn().mockResolvedValue("Missing param error") +const mockRecordToolError = vi.fn() +const mockReportSubagentProgress = vi.fn() +const mockFinalizeSubagentRunning = vi.fn() + +const mockTask = { + taskId: "parent-1", + consecutiveMistakeCount: 0, + didToolFailInCurrentTurn: false, + recordToolError: mockRecordToolError, + sayAndCreateMissingParamError: mockSayAndCreateMissingParamError, + say: mockSay, + reportSubagentProgress: mockReportSubagentProgress, + finalizeSubagentRunning: mockFinalizeSubagentRunning, + providerRef: { + deref: () => ({ + runSubagentInBackground: mockRunSubagentInBackground, + }), + }, +} + +describe("SubagentTool", () => { + beforeEach(() => { + vi.clearAllMocks() + mockRunSubagentInBackground.mockResolvedValue("Subagent completed successfully.") + }) + + describe("parseLegacy", () => { + it("parses description, prompt, and subagent_type", () => { + const params = subagentTool.parseLegacy({ + description: "Do something", + prompt: "Detailed instructions", + subagent_type: "explore", + }) + expect(params).toEqual({ + description: "Do something", + prompt: "Detailed instructions", + subagent_type: "explore", + }) + }) + + it("defaults subagent_type to general when invalid", () => { + const params = subagentTool.parseLegacy({ + description: "X", + prompt: "Y", + subagent_type: "invalid", + }) + expect(params.subagent_type).toBe("general") + }) + }) + + describe("execute", () => { + it("calls runSubagentInBackground and pushes result on success", async () => { + const block: ToolUse<"subagent"> = { + type: "tool_use", + name: "subagent", + params: {}, + partial: false, + nativeArgs: { + description: "Explore files", + prompt: "List files in src/", + subagent_type: "explore", + }, + } + + await subagentTool.execute(block.nativeArgs!, mockTask as any, { + askApproval: vi.fn(), + handleError: mockHandleError, + pushToolResult: mockPushToolResult, + }) + + expect(mockRunSubagentInBackground).toHaveBeenCalledWith( + expect.objectContaining({ + parentTaskId: "parent-1", + prompt: "List files in src/", + subagentType: "explore", + }), + ) + expect(mockRunSubagentInBackground).toHaveBeenCalledWith( + expect.objectContaining({ + onProgress: expect.any(Function), + }), + ) + expect(mockSay).toHaveBeenCalled() + expect(mockPushToolResult).toHaveBeenCalledWith("Subagent completed successfully.") + }) + + it("sends completed payload with resultCode/messageKey and pushes model message when result is structured (cancelled)", async () => { + mockRunSubagentInBackground.mockResolvedValue(SUBAGENT_CANCELLED_STRUCTURED_RESULT) + + await subagentTool.execute( + { description: "Do X", prompt: "Do it", subagent_type: "general" }, + mockTask as any, + { + askApproval: vi.fn(), + handleError: mockHandleError, + pushToolResult: mockPushToolResult, + }, + ) + + const sayCalls = mockSay.mock.calls + const completedCall = sayCalls.find((c) => { + try { + const payload = JSON.parse(c[1]) + return payload.tool === "subagentCompleted" + } catch { + return false + } + }) + expect(completedCall).toBeDefined() + const payload = JSON.parse(completedCall![1]) + expect(payload.resultCode).toBe("CANCELLED") + expect(payload.messageKey).toBe("chat:subagents.cancelledByUser") + expect(payload.result).toBe(SUBAGENT_CANCELLED_MODEL_MESSAGE) + expect(mockPushToolResult).toHaveBeenCalledWith(SUBAGENT_CANCELLED_MODEL_MESSAGE) + }) + + it("pushes error when description is missing", async () => { + await subagentTool.execute( + { description: "", prompt: "Do it", subagent_type: "general" }, + mockTask as any, + { + askApproval: vi.fn(), + handleError: mockHandleError, + pushToolResult: mockPushToolResult, + }, + ) + + expect(mockRunSubagentInBackground).not.toHaveBeenCalled() + expect(mockSayAndCreateMissingParamError).toHaveBeenCalledWith("subagent", "description") + expect(mockPushToolResult).toHaveBeenCalledWith("Missing param error") + }) + + it("pushes error when prompt is missing", async () => { + await subagentTool.execute({ description: "X", prompt: "", subagent_type: "general" }, mockTask as any, { + askApproval: vi.fn(), + handleError: mockHandleError, + pushToolResult: mockPushToolResult, + }) + + expect(mockRunSubagentInBackground).not.toHaveBeenCalled() + expect(mockSayAndCreateMissingParamError).toHaveBeenCalledWith("subagent", "prompt") + }) + + it("logs error and pushes generic tool error when runSubagentInBackground rejects (no raw message leak)", async () => { + mockRunSubagentInBackground.mockRejectedValue(new Error("API failed")) + + await subagentTool.execute({ description: "X", prompt: "Y", subagent_type: "general" }, mockTask as any, { + askApproval: vi.fn(), + handleError: mockHandleError, + pushToolResult: mockPushToolResult, + }) + + expect(mockHandleError).not.toHaveBeenCalled() + expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining(SUBAGENT_FAILED_MODEL_MESSAGE)) + expect(mockRecordToolError).toHaveBeenCalledWith("subagent") + const sayCalls = mockSay.mock.calls + const completedCall = sayCalls.find((c) => { + try { + const payload = JSON.parse(c[1]) + return payload.tool === "subagentCompleted" && payload.error !== undefined + } catch { + return false + } + }) + expect(completedCall).toBeDefined() + const payload = JSON.parse(completedCall![1]) + expect(payload.error).toBe(SUBAGENT_FAILED_MODEL_MESSAGE) + }) + }) + + describe("startAndReturnPromise and finish", () => { + it("startAndReturnPromise returns a Promise that resolves with the result and finish says completed and pushes", async () => { + mockRunSubagentInBackground.mockResolvedValue("Result from subagent") + mockSay.mockResolvedValue(undefined) + const params = { description: "Do X", prompt: "Do it", subagent_type: "general" as const } + const callbacks = { + askApproval: vi.fn(), + handleError: mockHandleError, + pushToolResult: mockPushToolResult, + toolCallId: "tool-call-123", + } + + const promise = subagentTool.startAndReturnPromise(mockTask as any, params, callbacks) + const result = await promise + expect(result).toBe("Result from subagent") + expect(mockRunSubagentInBackground).toHaveBeenCalledWith( + expect.objectContaining({ + parentTaskId: "parent-1", + prompt: "Do it", + subagentType: "general", + toolCallId: "tool-call-123", + }), + ) + + mockSay.mockClear() + mockPushToolResult.mockClear() + await subagentTool.finish(mockTask as any, params, result, callbacks) + const sayCalls = mockSay.mock.calls + const completedCall = sayCalls.find((c) => { + try { + const payload = JSON.parse(c[1]) + return payload.tool === "subagentCompleted" + } catch { + return false + } + }) + expect(completedCall).toBeDefined() + expect(JSON.parse(completedCall![1]).result).toBe("Result from subagent") + expect(mockPushToolResult).toHaveBeenCalledWith("Result from subagent") + }) + }) +}) diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 2ffe421c09..0602090c55 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -60,6 +60,11 @@ import { experimentDefault } from "../../shared/experiments" import { formatLanguage } from "../../shared/language" import { WebviewMessage } from "../../shared/WebviewMessage" import { EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels" +import { + SUBAGENT_CANCELLED_STRUCTURED_RESULT, + type RunSubagentInBackgroundParams, + type SubagentStructuredResult, +} from "../../shared/subagent" import { ProfileValidator } from "../../shared/ProfileValidator" import { Terminal } from "../../integrations/terminal/Terminal" @@ -2987,6 +2992,24 @@ export class ClineProvider return } + // If the current task has running subagents, cancel all of them and return (do not cancel the parent). + if (task.activeSubagentChildren.size > 0) { + const children = Array.from(task.activeSubagentChildren.entries()) + task.activeSubagentChildren.clear() + for (const [_toolCallId, subagentChild] of children) { + subagentChild.subagentProgressCallback = undefined + if (subagentChild.backgroundCompletionResolve) { + subagentChild.backgroundCompletionResolve(SUBAGENT_CANCELLED_STRUCTURED_RESULT) + subagentChild.backgroundCompletionResolve = undefined + } + subagentChild.abandoned = true + subagentChild.cancelCurrentRequest() + subagentChild.abortTask() + this.log(`[cancelTask] cancelled subagent ${subagentChild.taskId}.${subagentChild.instanceId}`) + } + return + } + console.log(`[cancelTask] cancelling task ${task.taskId}.${task.instanceId}`) let historyItem: HistoryItem | undefined @@ -3360,6 +3383,91 @@ export class ClineProvider return child } + /** + * Runs a subagent in the background. Parent stays current; child runs until attempt_completion. + * Resolves with the completion result string, or rejects on error. + */ + public async runSubagentInBackground( + params: RunSubagentInBackgroundParams, + ): Promise { + const { parentTaskId, prompt, subagentType, onProgress, toolCallId } = params + if (!toolCallId) { + throw new Error("[runSubagentInBackground] toolCallId is required") + } + const parent = this.getCurrentTask() + if (!parent) { + throw new Error("[runSubagentInBackground] No current task") + } + if (parent.taskId !== parentTaskId) { + throw new Error( + `[runSubagentInBackground] Parent mismatch: expected ${parentTaskId}, current ${parent.taskId}`, + ) + } + + const { apiConfiguration, organizationAllowList, enableCheckpoints, checkpointTimeout, experiments } = + await this.getState() + + if (!ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList)) { + throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist")) + } + + const child = new Task({ + provider: this, + apiConfiguration, + enableCheckpoints, + checkpointTimeout, + consecutiveMistakeLimit: apiConfiguration.consecutiveMistakeLimit, + task: prompt, + images: undefined, + experiments, + rootTask: this.clineStack.length > 0 ? this.clineStack[0] : undefined, + parentTask: parent, + taskNumber: this.clineStack.length + 1, + onCreated: this.taskCreationCallback, + startTask: false, + subagentType, + initialStatus: "active", + }) + if (onProgress) { + child.subagentProgressCallback = onProgress + } + + parent.activeSubagentChildren.set(toolCallId, child) + + return new Promise((resolve, reject) => { + const removeChild = () => { + parent.activeSubagentChildren.delete(toolCallId) + } + let settled = false + child.backgroundCompletionResolve = (result: string | SubagentStructuredResult) => { + if (!settled) { + settled = true + removeChild() + resolve(result) + } + } + child + .runBackgroundSubagentLoop(prompt) + .then(() => { + if (!settled) { + settled = true + removeChild() + reject(new Error("Subagent ended without attempt_completion")) + } + }) + .catch((err) => { + if (!settled) { + settled = true + removeChild() + reject(err) + } + }) + .finally(() => { + removeChild() + }) + }) + } + /** * Reopen parent task from delegation with write-back and events. */ diff --git a/src/shared/__tests__/experiments.spec.ts b/src/shared/__tests__/experiments.spec.ts index 92a7d7604f..3e8d149017 100644 --- a/src/shared/__tests__/experiments.spec.ts +++ b/src/shared/__tests__/experiments.spec.ts @@ -21,6 +21,7 @@ describe("experiments", () => { imageGeneration: false, runSlashCommand: false, customTools: false, + subagent: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(false) }) @@ -31,6 +32,7 @@ describe("experiments", () => { imageGeneration: false, runSlashCommand: false, customTools: false, + subagent: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(true) }) @@ -41,6 +43,7 @@ describe("experiments", () => { imageGeneration: false, runSlashCommand: false, customTools: false, + subagent: false, } expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(false) }) diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts index e189f99e23..f3b1fbc2de 100644 --- a/src/shared/experiments.ts +++ b/src/shared/experiments.ts @@ -5,6 +5,7 @@ export const EXPERIMENT_IDS = { IMAGE_GENERATION: "imageGeneration", RUN_SLASH_COMMAND: "runSlashCommand", CUSTOM_TOOLS: "customTools", + SUBAGENT: "subagent", } as const satisfies Record type _AssertExperimentIds = AssertEqual>> @@ -20,6 +21,7 @@ export const experimentConfigsMap: Record = { IMAGE_GENERATION: { enabled: false }, RUN_SLASH_COMMAND: { enabled: false }, CUSTOM_TOOLS: { enabled: false }, + SUBAGENT: { enabled: false }, } export const experimentDefault = Object.fromEntries( diff --git a/src/shared/subagent.ts b/src/shared/subagent.ts new file mode 100644 index 0000000000..dc2374f933 --- /dev/null +++ b/src/shared/subagent.ts @@ -0,0 +1,78 @@ +/** + * Shared constants and types for the subagent feature. + * Used by SubagentTool, Task, build-tools, and UI to stay in sync with tool message shapes. + */ + +/** Tool names used in say("tool", ...) payloads for subagent progress and completion. */ +export const SUBAGENT_TOOL_NAMES = { + running: "subagentRunning", + completed: "subagentCompleted", +} as const + +/** i18n keys sent as currentTask; webview uses t(key) to show locale. */ +export const SUBAGENT_STATUS_STARTING = "chat:subagents.starting" +export const SUBAGENT_STATUS_THINKING = "chat:subagents.thinking" + +/** Structured result codes for subagent completion; webview uses messageKey with t() for display. */ +export const SUBAGENT_RESULT_CODE_CANCELLED = "CANCELLED" as const +export const SUBAGENT_CANCELLED_MESSAGE_KEY = "chat:subagents.cancelledByUser" as const + +/** Sent when the user cancels the subagent. Extension passes this to backgroundCompletionResolve. */ +export const SUBAGENT_CANCELLED_STRUCTURED_RESULT = { + code: SUBAGENT_RESULT_CODE_CANCELLED, + messageKey: SUBAGENT_CANCELLED_MESSAGE_KEY, +} as const + +/** English message shown to the model when subagent is cancelled (tool result). */ +export const SUBAGENT_CANCELLED_MODEL_MESSAGE = "Subagent was cancelled by the user." + +/** Generic message shown to the model/UI when subagent fails (avoids leaking internal error details). */ +export const SUBAGENT_FAILED_MODEL_MESSAGE = "The subagent failed." + +export type SubagentStructuredResult = typeof SUBAGENT_CANCELLED_STRUCTURED_RESULT + +/** Payload for the "subagentRunning" tool message (progress updates). */ +export interface SubagentRunningPayload { + tool: typeof SUBAGENT_TOOL_NAMES.running + description?: string + currentTask?: string + /** Identifies this run for progress updates when multiple subagents run in parallel (e.g. block.id). */ + runId?: string +} + +/** Payload for the "subagentCompleted" tool message (result or error). */ +export interface SubagentCompletedPayload { + tool: typeof SUBAGENT_TOOL_NAMES.completed + description?: string + result?: string + error?: string + /** When set, webview shows t(messageKey) instead of result/error. */ + resultCode?: string + messageKey?: string +} + +export type SubagentType = "general" | "explore" + +/** Parameters for running a subagent in the background. */ +export interface RunSubagentInBackgroundParams { + parentTaskId: string + prompt: string + subagentType: SubagentType + onProgress?: (currentTask: string) => void + /** Required: keys the child in activeSubagentChildren and ties completion to the correct tool_use_id. */ + toolCallId: string +} + +/** Provider interface for running a subagent. Allows SubagentTool to call without type assertion. */ +export interface SubagentRunner { + runSubagentInBackground(params: RunSubagentInBackgroundParams): Promise +} + +export function isSubagentRunner(provider: unknown): provider is SubagentRunner { + return ( + typeof provider === "object" && + provider !== null && + "runSubagentInBackground" in provider && + typeof (provider as SubagentRunner).runSubagentInBackground === "function" + ) +} diff --git a/src/shared/tools.ts b/src/shared/tools.ts index d2dd9907b1..96d0abf1c7 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -57,6 +57,8 @@ export const toolParamNames = [ "end_line", "todos", "prompt", + "description", // subagent short label + "subagent_type", "image", // read_file parameters (native protocol) "operations", // search_and_replace parameter for multiple operations @@ -116,6 +118,7 @@ export type NativeToolArgs = { update_todo_list: { todos: string } use_mcp_tool: { server_name: string; tool_name: string; arguments?: Record } write_to_file: { path: string; content: string } + subagent: { description: string; prompt: string; subagent_type: "general" | "explore" } // Add more tools as they are migrated to native protocol } @@ -290,6 +293,7 @@ export const TOOL_DISPLAY_NAMES: Record = { skill: "load skill", generate_image: "generate images", custom_tool: "use custom tools", + subagent: "run subagent", } as const // Define available tool groups. @@ -322,6 +326,7 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [ "update_todo_list", "run_slash_command", "skill", + "subagent", ] as const /** diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 1f3f8f5a4f..1d5135f322 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -76,6 +76,50 @@ import { cn } from "@/lib/utils" import { PathTooltip } from "../ui/PathTooltip" import { OpenMarkdownPreviewButton } from "./OpenMarkdownPreviewButton" +/** i18n keys backend sends as currentTask; we pass to t() for display. */ +const SUBAGENT_STATUS_KEYS = ["chat:subagents.starting", "chat:subagents.thinking"] as const + +interface ParsedSubagentTask { + toolLabel: string + purpose: string | null + isGeneric: boolean +} + +/** + * Parses backend progress strings like "[read_file for 15 files]" or "Thinking..." into + * tool label and purpose for highlighted display. getToolLabel(key) returns the localized tool name. + */ +function parseSubagentCurrentTask(raw: string, getToolLabel: (toolKey: string) => string): ParsedSubagentTask { + const s = raw.trim() + if (!s) return { toolLabel: "", purpose: "...", isGeneric: true } + + const fallbackFormat = (str: string) => str.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()) + + // Generic status (no brackets) – show as-is + if (!s.startsWith("[")) { + return { toolLabel: "", purpose: s.endsWith("...") ? s : s + "...", isGeneric: true } + } + + // Strip brackets and parse "[tool_name for purpose]" + const inner = s.slice(1, s.endsWith("]") ? s.length - 1 : s.length).trim() + const forIndex = inner.indexOf(" for ") + if (forIndex === -1) { + const toolKey = inner.replace(/\s+/g, "_").toLowerCase() + const toolLabel = getToolLabel(toolKey) || fallbackFormat(inner) + return { toolLabel, purpose: null, isGeneric: false } + } + + const toolPart = inner.slice(0, forIndex).trim() + const purposePart = inner.slice(forIndex + 5).trim() + const toolKey = toolPart.replace(/\s+/g, "_").toLowerCase() + const toolLabel = getToolLabel(toolKey) || fallbackFormat(toolPart) + let purpose = purposePart + if ((purpose.startsWith("'") && purpose.endsWith("'")) || (purpose.startsWith('"') && purpose.endsWith('"'))) { + purpose = purpose.slice(1, -1) + } + return { toolLabel, purpose: purpose || null, isGeneric: false } +} + // Helper function to get previous todos before a specific message function getPreviousTodos(messages: ClineMessage[], currentMessageTs: number): any[] { // Find the previous updateTodoList message before the current one @@ -1518,6 +1562,91 @@ export const ChatRowContent = ({ ) } + case "subagentRunning": { + const desc = sayTool.description ?? "" + const currentTask = sayTool.currentTask + const getToolLabel = (key: string) => + t("chat:subagents.toolDisplayNames." + key, { + defaultValue: key.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase()), + }) + const parsed = currentTask ? parseSubagentCurrentTask(currentTask, getToolLabel) : null + const purposeKey = parsed?.purpose?.replace(/\.\.\.$/, "") + const displayPurpose = + purposeKey && + SUBAGENT_STATUS_KEYS.includes(purposeKey as (typeof SUBAGENT_STATUS_KEYS)[number]) + ? t(purposeKey) + : parsed?.purpose + return ( +
+
+ {message.progressStatus?.icon && ( + + )} + {t("chat:subagents.runningLabel", { description: desc })} +
+ {parsed && ( +
+ {parsed.toolLabel ? ( + displayPurpose ? ( + <> + {parsed.toolLabel}: {displayPurpose} + {!parsed.purpose?.endsWith("...") && "..."} + + ) : ( + <>{parsed.toolLabel}... + ) + ) : ( + <> + {displayPurpose?.endsWith("...") + ? displayPurpose + : `${displayPurpose ?? ""}...`} + + )} +
+ )} +
+ ) + } + case "subagentCompleted": { + const hasError = !!sayTool.error + return ( + <> +
+ + {t("chat:subagents.completedLabel")} + {sayTool.description && ( + + {sayTool.description} + + )} +
+ {(sayTool.result || + sayTool.error || + (sayTool.resultCode && sayTool.messageKey)) && ( +
+ {sayTool.resultCode === "CANCELLED" && sayTool.messageKey + ? t(sayTool.messageKey) + : hasError + ? sayTool.error + : sayTool.result} +
+ )} + + ) + } default: return null } diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index fd0aca66cb..82e1535471 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -561,6 +561,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction m.progressStatus?.spin === true)) { + return true + } + return false }, [modifiedMessages, clineAsk, enableButtons, primaryButtonText]) diff --git a/webview-ui/src/components/common/CodeAccordion.tsx b/webview-ui/src/components/common/CodeAccordion.tsx index 479e94a884..c3c9659814 100644 --- a/webview-ui/src/components/common/CodeAccordion.tsx +++ b/webview-ui/src/components/common/CodeAccordion.tsx @@ -1,6 +1,7 @@ import { memo, useMemo } from "react" import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" import { type ToolProgressStatus } from "@roo-code/types" +import { cn } from "@src/lib/utils" import { getLanguageFromPath } from "@src/utils/getLanguageFromPath" import { formatPathTooltip } from "@src/utils/formatPathTooltip" @@ -90,7 +91,14 @@ const CodeAccordion = ({ progressStatus.text && ( <> {progressStatus.icon && ( - + )} {progressStatus.text} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index ce7a607d9a..0bcc91eebc 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -373,11 +373,38 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode case "messageUpdated": { const clineMessage = message.clineMessage! setState((prevState) => { - // worth noting it will never be possible for a more up-to-date message to be sent here or in normal messages post since the presentAssistantContent function uses lock - const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === clineMessage.ts) - if (lastIndex !== -1) { + // For subagentRunning updates, match by runId so parallel subagents (same ts) each update their own row + let index = -1 + if ( + clineMessage.type === "say" && + clineMessage.say === "tool" && + typeof clineMessage.text === "string" + ) { + try { + const payload = JSON.parse(clineMessage.text) as { tool?: string; runId?: string } + if (payload.tool === "subagentRunning" && payload.runId != null) { + index = prevState.clineMessages.findIndex((msg) => { + if (msg.type !== "say" || msg.say !== "tool" || typeof msg.text !== "string") + return false + try { + const p = JSON.parse(msg.text) as { tool?: string; runId?: string } + return p.tool === "subagentRunning" && p.runId === payload.runId + } catch { + return false + } + }) + } + } catch { + // fall through to ts match + } + } + if (index === -1) { + // worth noting it will never be possible for a more up-to-date message to be sent here or in normal messages post since the presentAssistantContent function uses lock + index = findLastIndex(prevState.clineMessages, (msg) => msg.ts === clineMessage.ts) + } + if (index !== -1) { const newClineMessages = [...prevState.clineMessages] - newClineMessages[lastIndex] = clineMessage + newClineMessages[index] = clineMessage return { ...prevState, clineMessages: newClineMessages } } // Log a warning if messageUpdated arrives for a timestamp not in the diff --git a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx index 2a5e74c40d..23f0aa2d86 100644 --- a/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx +++ b/webview-ui/src/context/__tests__/ExtensionStateContext.spec.tsx @@ -234,6 +234,7 @@ describe("mergeExtensionState", () => { imageGeneration: false, runSlashCommand: false, customTools: false, + subagent: false, } as Record, checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS + 5, } @@ -250,6 +251,7 @@ describe("mergeExtensionState", () => { imageGeneration: false, runSlashCommand: false, customTools: false, + subagent: false, }) }) diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index 57f0acecd0..4786d96f81 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -269,6 +269,36 @@ "completionInstructions": "Subtasca completada! Pots revisar els resultats i suggerir correccions o següents passos. Si tot sembla correcte, confirma per tornar el resultat a la tasca principal.", "goToSubtask": "Veure tasca" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Roo té una pregunta" }, diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 4bd1b8ef33..30a337bdbd 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -826,6 +826,10 @@ "refreshSuccess": "Eines actualitzades correctament", "refreshError": "Error en actualitzar les eines", "toolParameters": "Paràmetres" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 727ec44c2d..35e67998c0 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -269,6 +269,36 @@ "completionInstructions": "Teilaufgabe abgeschlossen! Du kannst die Ergebnisse überprüfen und Korrekturen oder nächste Schritte vorschlagen. Wenn alles gut aussieht, bestätige, um das Ergebnis an die übergeordnete Aufgabe zurückzugeben.", "goToSubtask": "Aufgabe anzeigen" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Roo hat eine Frage" }, diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index c524ffb462..7a7c7ad62d 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -826,6 +826,10 @@ "refreshSuccess": "Tools erfolgreich aktualisiert", "refreshError": "Fehler beim Aktualisieren der Tools", "toolParameters": "Parameter" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 81338ad786..8bb64627c5 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -311,6 +311,36 @@ "completionInstructions": "You can review the results and suggest any corrections or next steps. If everything looks good, confirm to return the result to the parent task.", "goToSubtask": "View task" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Roo has a question" }, diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 183cd663e3..01098cb42f 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -889,6 +889,10 @@ "refreshSuccess": "Tools refreshed successfully", "refreshError": "Failed to refresh tools", "toolParameters": "Parameters" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index 1aa49f74f0..beb5a2433b 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -269,6 +269,36 @@ "completionInstructions": "¡Subtarea completada! Puedes revisar los resultados y sugerir correcciones o próximos pasos. Si todo se ve bien, confirma para devolver el resultado a la tarea principal.", "goToSubtask": "Ver tarea" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Roo tiene una pregunta" }, diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index dc2634b718..53815e26f8 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -826,6 +826,10 @@ "refreshSuccess": "Herramientas actualizadas correctamente", "refreshError": "Error al actualizar las herramientas", "toolParameters": "Parámetros" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 316a846199..4b8e1b661d 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -269,6 +269,36 @@ "completionInstructions": "Sous-tâche terminée ! Vous pouvez examiner les résultats et suggérer des corrections ou les prochaines étapes. Si tout semble bon, confirmez pour retourner le résultat à la tâche parente.", "goToSubtask": "Afficher la tâche" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Roo a une question" }, diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index ceed03755c..06b0a5fa7a 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -826,6 +826,10 @@ "refreshSuccess": "Outils actualisés avec succès", "refreshError": "Échec de l'actualisation des outils", "toolParameters": "Paramètres" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 410cd829ad..24c88ebb3d 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -269,6 +269,36 @@ "completionInstructions": "उपकार्य पूर्ण! आप परिणामों की समीक्षा कर सकते हैं और सुधार या अगले चरण सुझा सकते हैं। यदि सब कुछ ठीक लगता है, तो मुख्य कार्य को परिणाम वापस करने के लिए पुष्टि करें।", "goToSubtask": "कार्य देखें" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Roo का एक प्रश्न है" }, diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index a9e778e4a7..4849e988ca 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -826,6 +826,10 @@ "refreshSuccess": "टूल्स सफलतापूर्वक रिफ्रेश हुए", "refreshError": "टूल्स रिफ्रेश करने में विफल", "toolParameters": "पैरामीटर्स" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/id/chat.json b/webview-ui/src/i18n/locales/id/chat.json index c499024e4c..a349eec159 100644 --- a/webview-ui/src/i18n/locales/id/chat.json +++ b/webview-ui/src/i18n/locales/id/chat.json @@ -320,6 +320,36 @@ "completionInstructions": "Subtugas selesai! Kamu bisa meninjau hasilnya dan menyarankan koreksi atau langkah selanjutnya. Jika semuanya terlihat baik, konfirmasi untuk mengembalikan hasil ke tugas induk.", "goToSubtask": "Lihat tugas" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Roo punya pertanyaan" }, diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 36c0f6799f..41a2582486 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -826,6 +826,10 @@ "refreshSuccess": "Tool berhasil direfresh", "refreshError": "Gagal merefresh tool", "toolParameters": "Parameter" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index 006ee09a89..4115b90ec3 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -269,6 +269,36 @@ "completionInstructions": "Sottoattività completata! Puoi rivedere i risultati e suggerire correzioni o prossimi passi. Se tutto sembra a posto, conferma per restituire il risultato all'attività principale.", "goToSubtask": "Visualizza attività" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Roo ha una domanda" }, diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 970752b16b..c80e74e208 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -826,6 +826,10 @@ "refreshSuccess": "Strumenti aggiornati con successo", "refreshError": "Impossibile aggiornare gli strumenti", "toolParameters": "Parametri" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index d8bea631f3..cce7323637 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -269,6 +269,36 @@ "completionInstructions": "サブタスク完了!結果を確認し、修正や次のステップを提案できます。問題なければ、親タスクに結果を返すために確認してください。", "goToSubtask": "タスクを表示" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Rooは質問があります" }, diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index 1b2a125600..fc180434a8 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -826,6 +826,10 @@ "refreshSuccess": "ツールが正常に更新されました", "refreshError": "ツールの更新に失敗しました", "toolParameters": "パラメーター" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index c16ac36402..73e5e9a191 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -269,6 +269,36 @@ "completionInstructions": "하위 작업 완료! 결과를 검토하고 수정 사항이나 다음 단계를 제안할 수 있습니다. 모든 것이 괜찮아 보이면, 부모 작업에 결과를 반환하기 위해 확인해주세요.", "goToSubtask": "작업 보기" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Roo에게 질문이 있습니다" }, diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index e35ab51e3b..e8e9d52840 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -826,6 +826,10 @@ "refreshSuccess": "도구가 성공적으로 새로고침되었습니다", "refreshError": "도구 새로고침에 실패했습니다", "toolParameters": "매개변수" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 19a128f29d..13324309c7 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -264,6 +264,36 @@ "completionInstructions": "Subtaak voltooid! Je kunt de resultaten bekijken en eventuele correcties of volgende stappen voorstellen. Als alles goed is, bevestig dan om het resultaat terug te sturen naar de hoofdtaak.", "goToSubtask": "Taak weergeven" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Roo heeft een vraag" }, diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 9ec6a91df9..a2ab5a0e2c 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -826,6 +826,10 @@ "refreshSuccess": "Tools succesvol vernieuwd", "refreshError": "Fout bij vernieuwen van tools", "toolParameters": "Parameters" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index efbb47dad6..30621ba9fb 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -269,6 +269,36 @@ "completionInstructions": "Podzadanie zakończone! Możesz przejrzeć wyniki i zasugerować poprawki lub następne kroki. Jeśli wszystko wygląda dobrze, potwierdź, aby zwrócić wynik do zadania nadrzędnego.", "goToSubtask": "Wyświetl zadanie" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Roo ma pytanie" }, diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 45570384f1..5de3216b14 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -826,6 +826,10 @@ "refreshSuccess": "Narzędzia odświeżone pomyślnie", "refreshError": "Nie udało się odświeżyć narzędzi", "toolParameters": "Parametry" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index 6db70b0342..ab629a6d03 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -269,6 +269,36 @@ "completionInstructions": "Subtarefa concluída! Você pode revisar os resultados e sugerir correções ou próximos passos. Se tudo parecer bom, confirme para retornar o resultado à tarefa principal.", "goToSubtask": "Ver tarefa" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Roo tem uma pergunta" }, diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index cebf1c5cd2..9ee4980763 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -826,6 +826,10 @@ "refreshSuccess": "Ferramentas atualizadas com sucesso", "refreshError": "Falha ao atualizar ferramentas", "toolParameters": "Parâmetros" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index 50864bff80..90100c41aa 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -265,6 +265,36 @@ "completionInstructions": "Подзадача завершена! Вы можете просмотреть результаты и предложить исправления или следующие шаги. Если всё в порядке, подтвердите для возврата результата в родительскую задачу.", "goToSubtask": "Просмотреть задачу" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "У Roo есть вопрос" }, diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 698ae4c403..8e09b9e25a 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -826,6 +826,10 @@ "refreshSuccess": "Инструменты успешно обновлены", "refreshError": "Не удалось обновить инструменты", "toolParameters": "Параметры" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index 545e61d50e..12e0a289e2 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -270,6 +270,36 @@ "completionInstructions": "Alt görev tamamlandı! Sonuçları inceleyebilir ve düzeltmeler veya sonraki adımlar önerebilirsiniz. Her şey iyi görünüyorsa, sonucu üst göreve döndürmek için onaylayın.", "goToSubtask": "Görevi görüntüle" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Roo'nun bir sorusu var" }, diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 55ba2ece65..e5fbdeb5e8 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -826,6 +826,10 @@ "refreshSuccess": "Araçlar başarıyla yenilendi", "refreshError": "Araçlar yenilenemedi", "toolParameters": "Parametreler" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index e36c58b54a..c97af966e6 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -270,6 +270,36 @@ "completionInstructions": "Nhiệm vụ phụ đã hoàn thành! Bạn có thể xem lại kết quả và đề xuất các sửa đổi hoặc bước tiếp theo. Nếu mọi thứ có vẻ tốt, hãy xác nhận để trả kết quả về nhiệm vụ chính.", "goToSubtask": "Xem nhiệm vụ" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Roo có một câu hỏi" }, diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index 9d77fbc570..a90ac8d059 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -826,6 +826,10 @@ "refreshSuccess": "Làm mới công cụ thành công", "refreshError": "Không thể làm mới công cụ", "toolParameters": "Thông số" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 3e6f223e71..00b9bc7f2f 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -270,6 +270,36 @@ "completionInstructions": "子任务已完成!您可以查看结果并提出修改或下一步建议。如果一切正常,请确认以将结果返回给主任务。", "goToSubtask": "查看任务" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Roo有一个问题" }, diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index fea5dcc684..556886263c 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -826,6 +826,10 @@ "refreshSuccess": "工具刷新成功", "refreshError": "工具刷新失败", "toolParameters": "参数" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": { diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 4fe9c57843..3619f3518f 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -314,6 +314,36 @@ "completionInstructions": "子任務已完成!您可以檢閱結果並提出修正或後續步驟。如果一切順利,請確認以將結果回傳給主任務。", "goToSubtask": "查看工作" }, + "subagents": { + "runningLabel": "Running subagent: {{description}}", + "completedLabel": "Subagent completed", + "cancelledByUser": "Subagent was cancelled by the user.", + "starting": "Starting...", + "thinking": "Thinking...", + "toolDisplayNames": { + "read_file": "Read file", + "write_to_file": "Write file", + "apply_diff": "Apply diff", + "search_files": "Search files", + "search_and_replace": "Search and replace", + "search_replace": "Search replace", + "edit_file": "Edit file", + "list_files": "List files", + "codebase_search": "Codebase search", + "execute_command": "Run command", + "run_slash_command": "Slash command", + "ask_followup_question": "Ask question", + "attempt_completion": "Complete", + "update_todo_list": "Update todos", + "switch_mode": "Switch mode", + "new_task": "New task", + "generate_image": "Generate image", + "use_mcp_tool": "MCP tool", + "access_mcp_resource": "MCP resource", + "apply_patch": "Apply patch", + "subagent": "Subagent" + } + }, "questions": { "hasQuestion": "Roo 有一個問題" }, diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index 1a306043a3..627b11a0f8 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -836,6 +836,10 @@ "refreshSuccess": "工具重新整理成功", "refreshError": "工具重新整理失敗", "toolParameters": "參數" + }, + "SUBAGENT": { + "name": "Subagent tool", + "description": "When enabled, the model can run subagents in the background for research or sub-tasks. The subagent's result is returned to the main task. Use \"explore\" for read-only sub-tasks or \"general\" when the sub-task may edit files or run commands." } }, "promptCaching": {