This commit is contained in:
Ruslan Andreev 2026-04-16 17:04:44 +08:00 • committed by GitHub
commit b9d807f482
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 2058 additions and 39 deletions

View file

@ -6,7 +6,13 @@ import type { Keys, Equals, AssertEqual } from "./type-fu.js"
* ExperimentId * 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) export const experimentIdsSchema = z.enum(experimentIds)
@ -21,6 +27,7 @@ export const experimentsSchema = z.object({
imageGeneration: z.boolean().optional(), imageGeneration: z.boolean().optional(),
runSlashCommand: z.boolean().optional(), runSlashCommand: z.boolean().optional(),
customTools: z.boolean().optional(), customTools: z.boolean().optional(),
subagent: z.boolean().optional(),
}) })
export type Experiments = z.infer<typeof experimentsSchema> export type Experiments = z.infer<typeof experimentsSchema>

View file

@ -183,6 +183,7 @@ export type ClineSay = z.infer<typeof clineSaySchema>
export const toolProgressStatusSchema = z.object({ export const toolProgressStatusSchema = z.object({
icon: z.string().optional(), icon: z.string().optional(),
text: z.string().optional(), text: z.string().optional(),
spin: z.boolean().optional(),
}) })
export type ToolProgressStatus = z.infer<typeof toolProgressStatusSchema> export type ToolProgressStatus = z.infer<typeof toolProgressStatusSchema>

View file

@ -46,6 +46,7 @@ export const toolNames = [
"skill", "skill",
"generate_image", "generate_image",
"custom_tool", "custom_tool",
"subagent",
] as const ] as const
export const toolNamesSchema = z.enum(toolNames) export const toolNamesSchema = z.enum(toolNames)

View file

@ -783,6 +783,8 @@ export interface ClineSayTool {
| "runSlashCommand" | "runSlashCommand"
| "updateTodoList" | "updateTodoList"
| "skill" | "skill"
| "subagentRunning"
| "subagentCompleted"
path?: string path?: string
// For readCommandOutput // For readCommandOutput
readStart?: number readStart?: number
@ -840,6 +842,13 @@ export interface ClineSayTool {
description?: string description?: string
// Properties for skill tool // Properties for skill tool
skill?: string 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 { export interface ClineAskUseMcpServer {

View file

@ -637,6 +637,20 @@ export class NativeToolCallParser {
} }
break 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: default:
break break
} }
@ -911,6 +925,21 @@ export class NativeToolCallParser {
} }
break 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<TName>
}
break
case "use_mcp_tool": case "use_mcp_tool":
if (args.server_name !== undefined && args.tool_name !== undefined) { if (args.server_name !== undefined && args.tool_name !== undefined) {
nativeArgs = { nativeArgs = {

View file

@ -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", () => { describe("processStreamingChunk", () => {

View file

@ -8,6 +8,7 @@ import { customToolRegistry } from "@roo-code/core"
import { t } from "../../i18n" import { t } from "../../i18n"
import { SUBAGENT_STATUS_THINKING } from "../../shared/subagent"
import { defaultModeSlug, getModeBySlug } from "../../shared/modes" import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../shared/tools" import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../shared/tools"
@ -30,6 +31,7 @@ import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool"
import { switchModeTool } from "../tools/SwitchModeTool" import { switchModeTool } from "../tools/SwitchModeTool"
import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/AttemptCompletionTool" import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/AttemptCompletionTool"
import { newTaskTool } from "../tools/NewTaskTool" import { newTaskTool } from "../tools/NewTaskTool"
import { subagentTool } from "../tools/SubagentTool"
import { updateTodoListTool } from "../tools/UpdateTodoListTool" import { updateTodoListTool } from "../tools/UpdateTodoListTool"
import { runSlashCommandTool } from "../tools/RunSlashCommandTool" import { runSlashCommandTool } from "../tools/RunSlashCommandTool"
import { skillTool } from "../tools/SkillTool" import { skillTool } from "../tools/SkillTool"
@ -85,6 +87,7 @@ export async function presentAssistantMessage(cline: Task) {
} }
let block: any let block: any
let blocksConsumed = 1
try { try {
// Performance optimization: Use shallow copy instead of deep clone. // Performance optimization: Use shallow copy instead of deep clone.
// The block is used read-only throughout this function - we never mutate its properties. // 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, "") content = content.replace(/\s?<\/thinking>/g, "")
} }
cline.subagentProgressCallback?.(SUBAGENT_STATUS_THINKING)
await cline.say("text", content, undefined, block.partial) await cline.say("text", content, undefined, block.partial)
break 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}` : ""}]` return `[${block.name} for '${block.params.skill}'${block.params.args ? ` with args: ${block.params.args}` : ""}]`
case "generate_image": case "generate_image":
return `[${block.name} for '${block.params.path}']` return `[${block.name} for '${block.params.path}']`
case "subagent":
return `[${block.name}: ${block.params.description ?? "(no description)"}]`
default: default:
return `[${block.name}]` return `[${block.name}]`
} }
@ -405,8 +411,36 @@ export async function presentAssistantMessage(cline: Task) {
break break
} }
// Track if we've already pushed a tool result for this tool call (native tool calling only) // Track which tool_use_ids have already received a tool_result (native tool calling only)
let hasToolResult = false const toolResultsPushed = new Set<string>()
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 // 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 // (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 let approvalFeedback: { text: string; images?: string[] } | undefined
const pushToolResult = (content: ToolResponse) => { 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 resultContent: string
let imageBlocks: Anthropic.ImageBlockParam[] = [] let imageBlocks: Anthropic.ImageBlockParam[] = []
if (typeof content === "string") { if (typeof content === "string") {
resultContent = content || "(tool did not return anything)" resultContent = content || "(tool did not return anything)"
} else { } else {
@ -467,7 +492,6 @@ export async function presentAssistantMessage(cline: Task) {
textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") || textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") ||
"(tool did not return anything)" "(tool did not return anything)"
} }
// Merge approval feedback into tool result (GitHub #10465) // Merge approval feedback into tool result (GitHub #10465)
if (approvalFeedback) { if (approvalFeedback) {
const feedbackText = formatResponse.toolApprovedWithFeedback(approvalFeedback.text) const feedbackText = formatResponse.toolApprovedWithFeedback(approvalFeedback.text)
@ -477,18 +501,9 @@ export async function presentAssistantMessage(cline: Task) {
imageBlocks = [...feedbackImageBlocks, ...imageBlocks] imageBlocks = [...feedbackImageBlocks, ...imageBlocks]
} }
} }
const mergedContent: ToolResponse =
cline.pushToolResultToUserContent({ imageBlocks.length > 0 ? [{ type: "text", text: resultContent }, ...imageBlocks] : resultContent
type: "tool_result", pushToolResultFor(toolCallId, mergedContent)
tool_use_id: sanitizeToolUseId(toolCallId),
content: resultContent,
})
if (imageBlocks.length > 0) {
cline.userMessageContent.push(...imageBlocks)
}
hasToolResult = true
} }
const askApproval = async ( const askApproval = async (
@ -675,6 +690,7 @@ export async function presentAssistantMessage(cline: Task) {
} }
} }
cline.subagentProgressCallback?.(toolDescription())
switch (block.name) { switch (block.name) {
case "write_to_file": case "write_to_file":
await checkpointSaveAndMark(cline) await checkpointSaveAndMark(cline)
@ -812,6 +828,92 @@ export async function presentAssistantMessage(cline: Task) {
toolCallId: block.id, toolCallId: block.id,
}) })
break 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<string, string>
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<string, string>)
}
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": { case "attempt_completion": {
const completionCallbacks: AttemptCompletionCallbacks = { const completionCallbacks: AttemptCompletionCallbacks = {
askApproval, askApproval,
@ -939,7 +1041,7 @@ export async function presentAssistantMessage(cline: Task) {
// (instead of preemptively doing it in iterator). // (instead of preemptively doing it in iterator).
if (!block.partial || cline.didRejectTool || cline.didAlreadyUseTool) { if (!block.partial || cline.didRejectTool || cline.didAlreadyUseTool) {
// Block is finished streaming and executing. // 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 // It's okay that we increment if !didCompleteReadingStream, it'll
// just return because out of bounds and as streaming continues it // just return because out of bounds and as streaming continues it
// will call `presentAssitantMessage` if a new block is ready. If // 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 // Call next block if it exists (if not then read stream will call it
// when it's ready). // when it's ready).
// Need to increment regardless, so when read stream calls this function // Need to increment by blocksConsumed (e.g. 1 normally, or N when we processed a parallel subagent batch).
// again it will be streaming the next block. cline.currentStreamingContentIndex += blocksConsumed
cline.currentStreamingContentIndex++
if (cline.currentStreamingContentIndex < cline.assistantMessageContent.length) { if (cline.currentStreamingContentIndex < cline.assistantMessageContent.length) {
// There are already more content blocks to stream, so we'll call // There are already more content blocks to stream, so we'll call

View file

@ -89,3 +89,55 @@ describe("filterNativeToolsForMode - disabledTools", () => {
expect(resultNames).not.toContain("edit") 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")
})
})

View file

@ -291,6 +291,10 @@ export function filterNativeToolsForMode(
allowedToolNames.delete("run_slash_command") allowedToolNames.delete("run_slash_command")
} }
if (!experiments?.subagent) {
allowedToolNames.delete("subagent")
}
// Remove tools that are explicitly disabled via the disabledTools setting // Remove tools that are explicitly disabled via the disabledTools setting
if (settings?.disabledTools?.length) { if (settings?.disabledTools?.length) {
for (const toolName of settings.disabledTools) { for (const toolName of settings.disabledTools) {
@ -379,6 +383,9 @@ export function isToolAllowedInMode(
if (toolName === "run_slash_command") { if (toolName === "run_slash_command") {
return experiments?.runSlashCommand === true return experiments?.runSlashCommand === true
} }
if (toolName === "subagent") {
return experiments?.subagent === true
}
return true return true
} }

View file

@ -13,6 +13,7 @@ import newTask from "./new_task"
import readCommandOutput from "./read_command_output" import readCommandOutput from "./read_command_output"
import { createReadFileTool, type ReadFileToolOptions } from "./read_file" import { createReadFileTool, type ReadFileToolOptions } from "./read_file"
import runSlashCommand from "./run_slash_command" import runSlashCommand from "./run_slash_command"
import subagent from "./subagent"
import skill from "./skill" import skill from "./skill"
import searchReplace from "./search_replace" import searchReplace from "./search_replace"
import edit_file from "./edit_file" import edit_file from "./edit_file"
@ -60,6 +61,7 @@ export function getNativeTools(options: NativeToolsOptions = {}): OpenAI.Chat.Ch
readCommandOutput, readCommandOutput,
createReadFileTool(readFileOptions), createReadFileTool(readFileOptions),
runSlashCommand, runSlashCommand,
subagent,
skill, skill,
searchReplace, searchReplace,
edit_file, edit_file,

View file

@ -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

View file

@ -70,6 +70,7 @@ import { t } from "../../i18n"
import { getApiMetrics, hasTokenUsageChanged, hasToolUsageChanged } from "../../shared/getApiMetrics" import { getApiMetrics, hasTokenUsageChanged, hasToolUsageChanged } from "../../shared/getApiMetrics"
import { ClineAskResponse } from "../../shared/WebviewMessage" import { ClineAskResponse } from "../../shared/WebviewMessage"
import { defaultModeSlug, getModeBySlug } from "../../shared/modes" 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 { DiffStrategy, type ToolUse, type ToolParamName, toolParamNames } from "../../shared/tools"
import { getModelMaxOutputTokens } from "../../shared/api" import { getModelMaxOutputTokens } from "../../shared/api"
@ -157,6 +158,10 @@ export interface TaskOptions extends CreateTaskOptions {
workspacePath?: string workspacePath?: string
/** Initial status for the task's history item (e.g., "active" for child tasks) */ /** Initial status for the task's history item (e.g., "active" for child tasks) */
initialStatus?: "active" | "delegated" | "completed" 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<TaskEvents> implements TaskLike { export class Task extends EventEmitter<TaskEvents> implements TaskLike {
@ -417,6 +422,17 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// MessageManager for high-level message operations (lazy initialized) // MessageManager for high-level message operations (lazy initialized)
private _messageManager?: MessageManager 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<string, Task>()
constructor({ constructor({
provider, provider,
apiConfiguration, apiConfiguration,
@ -436,9 +452,14 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
initialTodos, initialTodos,
workspacePath, workspacePath,
initialStatus, initialStatus,
subagentType,
needUpdateHistory,
}: TaskOptions) { }: TaskOptions) {
super() super()
this.subagentType = subagentType
this.needUpdateHistory = needUpdateHistory ?? subagentType === undefined
if (startTask && !task && !images && !historyItem) { if (startTask && !task && !images && !historyItem) {
throw new Error("Either historyItem or task/images must be provided") throw new Error("Either historyItem or task/images must be provided")
} }
@ -1240,7 +1261,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// - Final state is emitted when updates stop (trailing: true) // - Final state is emitted when updates stop (trailing: true)
this.debouncedEmitTokenUsage(tokenUsage, this.toolUsage) this.debouncedEmitTokenUsage(tokenUsage, this.toolUsage)
await this.providerRef.deref()?.updateTaskHistory(historyItem) if (this.needUpdateHistory) {
await this.providerRef.deref()?.updateTaskHistory(historyItem)
}
return true return true
} catch (error) { } catch (error) {
console.error("Failed to save Roo messages:", error) console.error("Failed to save Roo messages:", error)
@ -1258,6 +1281,51 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
return undefined 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), // Note that `partial` has three valid states true (partial message),
// false (completion of partial message), undefined (individual complete // false (completion of partial message), undefined (individual complete
// message). // message).
@ -1674,6 +1742,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
disabledTools: state?.disabledTools, disabledTools: state?.disabledTools,
modelInfo, modelInfo,
includeAllToolsWithRestrictions: false, includeAllToolsWithRestrictions: false,
subagentType: this.subagentType,
}) })
allTools = toolsResult.tools allTools = toolsResult.tools
} }
@ -1798,6 +1867,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
text, text,
images, images,
partial, partial,
progressStatus,
contextCondense, contextCondense,
contextTruncation, contextTruncation,
}) })
@ -1836,6 +1906,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
say: type, say: type,
text, text,
images, images,
progressStatus,
contextCondense, contextCondense,
contextTruncation, contextTruncation,
}) })
@ -1860,6 +1931,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
text, text,
images, images,
checkpoint, checkpoint,
progressStatus,
contextCondense, contextCondense,
contextTruncation, contextTruncation,
}) })
@ -1876,6 +1948,28 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
return formatResponse.toolError(formatResponse.missingToolParameterError(paramName)) 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<void> {
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: `<task>\n${taskContent}\n</task>` }]).catch((error) => {
if (this.abandoned === true || this.backgroundCompletionResolve === undefined) {
return
}
throw error
})
}
// Lifecycle // Lifecycle
// Start / Resume / Abort / Dispose // Start / Resume / Abort / Dispose
@ -3866,6 +3960,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
disabledTools: state?.disabledTools, disabledTools: state?.disabledTools,
modelInfo, modelInfo,
includeAllToolsWithRestrictions: false, includeAllToolsWithRestrictions: false,
subagentType: this.subagentType,
}) })
allTools = toolsResult.tools allTools = toolsResult.tools
} }
@ -4080,6 +4175,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
disabledTools: state?.disabledTools, disabledTools: state?.disabledTools,
modelInfo, modelInfo,
includeAllToolsWithRestrictions: false, includeAllToolsWithRestrictions: false,
subagentType: this.subagentType,
}) })
contextMgmtTools = toolsResult.tools contextMgmtTools = toolsResult.tools
} }
@ -4244,6 +4340,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
disabledTools: state?.disabledTools, disabledTools: state?.disabledTools,
modelInfo, modelInfo,
includeAllToolsWithRestrictions: supportsAllowedFunctionNames, includeAllToolsWithRestrictions: supportsAllowedFunctionNames,
subagentType: this.subagentType,
}) })
allTools = toolsResult.tools allTools = toolsResult.tools
allowedFunctionNames = toolsResult.allowedFunctionNames allowedFunctionNames = toolsResult.allowedFunctionNames

View file

@ -448,3 +448,164 @@ describe("flushPendingToolResultsToHistory", () => {
expect(task.apiConversationHistory.length).toBe(0) 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],
})
})
})

View file

@ -2,7 +2,7 @@ import path from "path"
import type OpenAI from "openai" 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 { customToolRegistry, formatNative } from "@roo-code/core"
import type { ClineProvider } from "../webview/ClineProvider" import type { ClineProvider } from "../webview/ClineProvider"
@ -14,6 +14,42 @@ import {
filterMcpToolsForMode, filterMcpToolsForMode,
resolveToolAlias, resolveToolAlias,
} from "../prompts/tools/filter-tools-for-mode" } 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<string> = 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<T extends OpenAI.Chat.ChatCompletionTool>(
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 { interface BuildToolsOptions {
provider: ClineProvider provider: ClineProvider
@ -31,6 +67,8 @@ interface BuildToolsOptions {
* to pass all tool definitions while restricting callable tools. * to pass all tool definitions while restricting callable tools.
*/ */
includeAllToolsWithRestrictions?: boolean includeAllToolsWithRestrictions?: boolean
/** When "explore", only read-only tools are allowed (no edit/command) */
subagentType?: "general" | "explore"
} }
interface BuildToolsResult { interface BuildToolsResult {
@ -90,6 +128,7 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO
disabledTools, disabledTools,
modelInfo, modelInfo,
includeAllToolsWithRestrictions, includeAllToolsWithRestrictions,
subagentType,
} = options } = options
const mcpHub = provider.getMcpHub() const mcpHub = provider.getMcpHub()
@ -141,8 +180,9 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO
} }
} }
// Combine filtered tools (for backward compatibility and for allowedFunctionNames) // Combine filtered tools, then apply subagent restrictions when this task is a subagent
const filteredTools = [...filteredNativeTools, ...filteredMcpTools, ...nativeCustomTools] const combined = [...filteredNativeTools, ...filteredMcpTools, ...nativeCustomTools]
const filteredTools = applySubagentToolRestrictions(combined, subagentType, getToolName, resolveToolAlias)
// If includeAllToolsWithRestrictions is true, return ALL tools but provide // If includeAllToolsWithRestrictions is true, return ALL tools but provide
// allowed names based on mode filtering // allowed names based on mode filtering
@ -150,7 +190,7 @@ export async function buildNativeToolsArrayWithRestrictions(options: BuildToolsO
// Combine ALL tools (unfiltered native + all MCP + custom) // Combine ALL tools (unfiltered native + all MCP + custom)
const allTools = [...nativeTools, ...mcpTools, ...nativeCustomTools] 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 // Resolve any alias names to canonical names to ensure consistency with allTools
// (which uses canonical names). This prevents Gemini errors when tools are renamed // (which uses canonical names). This prevents Gemini errors when tools are renamed
// to aliases in filteredTools but allTools contains the original canonical names. // to aliases in filteredTools but allTools contains the original canonical names.

View file

@ -80,6 +80,19 @@ export class AttemptCompletionTool extends BaseTool<"attempt_completion"> {
await task.say("completion_result", result, undefined, false) 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) // Check for subtask using parentTaskId (metadata-driven delegation)
if (task.parentTaskId) { if (task.parentTaskId) {
// Check if this subtask has already completed and returned to parent // Check if this subtask has already completed and returned to parent

View file

@ -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<Record<string, string>>): 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<string | SubagentStructuredResult> {
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<void> {
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<void> {
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()

View file

@ -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<string | SubagentStructuredResult>
>()
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")
})
})
})

View file

@ -60,6 +60,11 @@ import { experimentDefault } from "../../shared/experiments"
import { formatLanguage } from "../../shared/language" import { formatLanguage } from "../../shared/language"
import { WebviewMessage } from "../../shared/WebviewMessage" import { WebviewMessage } from "../../shared/WebviewMessage"
import { EMBEDDING_MODEL_PROFILES } from "../../shared/embeddingModels" 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 { ProfileValidator } from "../../shared/ProfileValidator"
import { Terminal } from "../../integrations/terminal/Terminal" import { Terminal } from "../../integrations/terminal/Terminal"
@ -2987,6 +2992,24 @@ export class ClineProvider
return 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}`) console.log(`[cancelTask] cancelling task ${task.taskId}.${task.instanceId}`)
let historyItem: HistoryItem | undefined let historyItem: HistoryItem | undefined
@ -3360,6 +3383,91 @@ export class ClineProvider
return child 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<string | SubagentStructuredResult> {
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<string | SubagentStructuredResult>((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. * Reopen parent task from delegation with write-back and events.
*/ */

View file

@ -21,6 +21,7 @@ describe("experiments", () => {
imageGeneration: false, imageGeneration: false,
runSlashCommand: false, runSlashCommand: false,
customTools: false, customTools: false,
subagent: false,
} }
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(false) expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(false)
}) })
@ -31,6 +32,7 @@ describe("experiments", () => {
imageGeneration: false, imageGeneration: false,
runSlashCommand: false, runSlashCommand: false,
customTools: false, customTools: false,
subagent: false,
} }
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(true) expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(true)
}) })
@ -41,6 +43,7 @@ describe("experiments", () => {
imageGeneration: false, imageGeneration: false,
runSlashCommand: false, runSlashCommand: false,
customTools: false, customTools: false,
subagent: false,
} }
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(false) expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.PREVENT_FOCUS_DISRUPTION)).toBe(false)
}) })

View file

@ -5,6 +5,7 @@ export const EXPERIMENT_IDS = {
IMAGE_GENERATION: "imageGeneration", IMAGE_GENERATION: "imageGeneration",
RUN_SLASH_COMMAND: "runSlashCommand", RUN_SLASH_COMMAND: "runSlashCommand",
CUSTOM_TOOLS: "customTools", CUSTOM_TOOLS: "customTools",
SUBAGENT: "subagent",
} as const satisfies Record<string, ExperimentId> } as const satisfies Record<string, ExperimentId>
type _AssertExperimentIds = AssertEqual<Equals<ExperimentId, Values<typeof EXPERIMENT_IDS>>> type _AssertExperimentIds = AssertEqual<Equals<ExperimentId, Values<typeof EXPERIMENT_IDS>>>
@ -20,6 +21,7 @@ export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
IMAGE_GENERATION: { enabled: false }, IMAGE_GENERATION: { enabled: false },
RUN_SLASH_COMMAND: { enabled: false }, RUN_SLASH_COMMAND: { enabled: false },
CUSTOM_TOOLS: { enabled: false }, CUSTOM_TOOLS: { enabled: false },
SUBAGENT: { enabled: false },
} }
export const experimentDefault = Object.fromEntries( export const experimentDefault = Object.fromEntries(

78
src/shared/subagent.ts Normal file
View file

@ -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<string | SubagentStructuredResult>
}
export function isSubagentRunner(provider: unknown): provider is SubagentRunner {
return (
typeof provider === "object" &&
provider !== null &&
"runSubagentInBackground" in provider &&
typeof (provider as SubagentRunner).runSubagentInBackground === "function"
)
}

View file

@ -57,6 +57,8 @@ export const toolParamNames = [
"end_line", "end_line",
"todos", "todos",
"prompt", "prompt",
"description", // subagent short label
"subagent_type",
"image", "image",
// read_file parameters (native protocol) // read_file parameters (native protocol)
"operations", // search_and_replace parameter for multiple operations "operations", // search_and_replace parameter for multiple operations
@ -116,6 +118,7 @@ export type NativeToolArgs = {
update_todo_list: { todos: string } update_todo_list: { todos: string }
use_mcp_tool: { server_name: string; tool_name: string; arguments?: Record<string, unknown> } use_mcp_tool: { server_name: string; tool_name: string; arguments?: Record<string, unknown> }
write_to_file: { path: string; content: string } 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 // Add more tools as they are migrated to native protocol
} }
@ -290,6 +293,7 @@ export const TOOL_DISPLAY_NAMES: Record<ToolName, string> = {
skill: "load skill", skill: "load skill",
generate_image: "generate images", generate_image: "generate images",
custom_tool: "use custom tools", custom_tool: "use custom tools",
subagent: "run subagent",
} as const } as const
// Define available tool groups. // Define available tool groups.
@ -322,6 +326,7 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [
"update_todo_list", "update_todo_list",
"run_slash_command", "run_slash_command",
"skill", "skill",
"subagent",
] as const ] as const
/** /**

View file

@ -76,6 +76,50 @@ import { cn } from "@/lib/utils"
import { PathTooltip } from "../ui/PathTooltip" import { PathTooltip } from "../ui/PathTooltip"
import { OpenMarkdownPreviewButton } from "./OpenMarkdownPreviewButton" 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 // Helper function to get previous todos before a specific message
function getPreviousTodos(messages: ClineMessage[], currentMessageTs: number): any[] { function getPreviousTodos(messages: ClineMessage[], currentMessageTs: number): any[] {
// Find the previous updateTodoList message before the current one // Find the previous updateTodoList message before the current one
@ -1518,6 +1562,91 @@ export const ChatRowContent = ({
</div> </div>
) )
} }
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 (
<div>
<div className="flex items-center gap-2.5 mb-1 break-words">
{message.progressStatus?.icon && (
<span
className={cn(
"codicon",
`codicon-${message.progressStatus.icon}`,
"mr-1.5 text-vscode-foreground",
message.progressStatus.spin && "codicon-modifier-spin",
)}
/>
)}
<span>{t("chat:subagents.runningLabel", { description: desc })}</span>
</div>
{parsed && (
<div className="pl-6 mt-1.5 text-sm text-vscode-descriptionForeground">
{parsed.toolLabel ? (
displayPurpose ? (
<>
{parsed.toolLabel}: {displayPurpose}
{!parsed.purpose?.endsWith("...") && "..."}
</>
) : (
<>{parsed.toolLabel}...</>
)
) : (
<>
{displayPurpose?.endsWith("...")
? displayPurpose
: `${displayPurpose ?? ""}...`}
</>
)}
</div>
)}
</div>
)
}
case "subagentCompleted": {
const hasError = !!sayTool.error
return (
<>
<div className="flex items-center gap-2.5 mb-1 break-words">
<span
className={cn(
`codicon codicon-${hasError ? "error" : "check-all"}`,
"mr-1.5",
hasError ? "text-vscode-errorForeground" : "text-vscode-foreground",
)}
/>
<span>{t("chat:subagents.completedLabel")}</span>
{sayTool.description && (
<span className="text-vscode-descriptionForeground ml-2">
{sayTool.description}
</span>
)}
</div>
{(sayTool.result ||
sayTool.error ||
(sayTool.resultCode && sayTool.messageKey)) && (
<div className="pl-6 mt-2 text-vscode-descriptionForeground whitespace-pre-wrap">
{sayTool.resultCode === "CANCELLED" && sayTool.messageKey
? t(sayTool.messageKey)
: hasError
? sayTool.error
: sayTool.result}
</div>
)}
</>
)
}
default: default:
return null return null
} }

View file

@ -561,6 +561,12 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
} }
} }
// Check if any subagent is currently running (has a spinning progress indicator).
// The parent task's API call is already finished, but we still want the stop button.
if (modifiedMessages.some((m) => m.progressStatus?.spin === true)) {
return true
}
return false return false
}, [modifiedMessages, clineAsk, enableButtons, primaryButtonText]) }, [modifiedMessages, clineAsk, enableButtons, primaryButtonText])

View file

@ -1,6 +1,7 @@
import { memo, useMemo } from "react" import { memo, useMemo } from "react"
import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react" import { VSCodeProgressRing } from "@vscode/webview-ui-toolkit/react"
import { type ToolProgressStatus } from "@roo-code/types" import { type ToolProgressStatus } from "@roo-code/types"
import { cn } from "@src/lib/utils"
import { getLanguageFromPath } from "@src/utils/getLanguageFromPath" import { getLanguageFromPath } from "@src/utils/getLanguageFromPath"
import { formatPathTooltip } from "@src/utils/formatPathTooltip" import { formatPathTooltip } from "@src/utils/formatPathTooltip"
@ -90,7 +91,14 @@ const CodeAccordion = ({
progressStatus.text && ( progressStatus.text && (
<> <>
{progressStatus.icon && ( {progressStatus.icon && (
<span className={`codicon codicon-${progressStatus.icon} mr-1`} /> <span
className={cn(
"codicon",
`codicon-${progressStatus.icon}`,
"mr-1",
progressStatus.spin && "codicon-modifier-spin",
)}
/>
)} )}
<span className="mr-1 ml-auto text-vscode-descriptionForeground"> <span className="mr-1 ml-auto text-vscode-descriptionForeground">
{progressStatus.text} {progressStatus.text}

View file

@ -373,11 +373,38 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
case "messageUpdated": { case "messageUpdated": {
const clineMessage = message.clineMessage! const clineMessage = message.clineMessage!
setState((prevState) => { 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 // For subagentRunning updates, match by runId so parallel subagents (same ts) each update their own row
const lastIndex = findLastIndex(prevState.clineMessages, (msg) => msg.ts === clineMessage.ts) let index = -1
if (lastIndex !== -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] const newClineMessages = [...prevState.clineMessages]
newClineMessages[lastIndex] = clineMessage newClineMessages[index] = clineMessage
return { ...prevState, clineMessages: newClineMessages } return { ...prevState, clineMessages: newClineMessages }
} }
// Log a warning if messageUpdated arrives for a timestamp not in the // Log a warning if messageUpdated arrives for a timestamp not in the

View file

@ -234,6 +234,7 @@ describe("mergeExtensionState", () => {
imageGeneration: false, imageGeneration: false,
runSlashCommand: false, runSlashCommand: false,
customTools: false, customTools: false,
subagent: false,
} as Record<ExperimentId, boolean>, } as Record<ExperimentId, boolean>,
checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS + 5, checkpointTimeout: DEFAULT_CHECKPOINT_TIMEOUT_SECONDS + 5,
} }
@ -250,6 +251,7 @@ describe("mergeExtensionState", () => {
imageGeneration: false, imageGeneration: false,
runSlashCommand: false, runSlashCommand: false,
customTools: false, customTools: false,
subagent: false,
}) })
}) })

View file

@ -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.", "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" "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": { "questions": {
"hasQuestion": "Roo té una pregunta" "hasQuestion": "Roo té una pregunta"
}, },

View file

@ -826,6 +826,10 @@
"refreshSuccess": "Eines actualitzades correctament", "refreshSuccess": "Eines actualitzades correctament",
"refreshError": "Error en actualitzar les eines", "refreshError": "Error en actualitzar les eines",
"toolParameters": "Paràmetres" "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": { "promptCaching": {

View file

@ -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.", "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" "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": { "questions": {
"hasQuestion": "Roo hat eine Frage" "hasQuestion": "Roo hat eine Frage"
}, },

View file

@ -826,6 +826,10 @@
"refreshSuccess": "Tools erfolgreich aktualisiert", "refreshSuccess": "Tools erfolgreich aktualisiert",
"refreshError": "Fehler beim Aktualisieren der Tools", "refreshError": "Fehler beim Aktualisieren der Tools",
"toolParameters": "Parameter" "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": { "promptCaching": {

View file

@ -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.", "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" "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": { "questions": {
"hasQuestion": "Roo has a question" "hasQuestion": "Roo has a question"
}, },

View file

@ -889,6 +889,10 @@
"refreshSuccess": "Tools refreshed successfully", "refreshSuccess": "Tools refreshed successfully",
"refreshError": "Failed to refresh tools", "refreshError": "Failed to refresh tools",
"toolParameters": "Parameters" "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": { "promptCaching": {

View file

@ -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.", "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" "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": { "questions": {
"hasQuestion": "Roo tiene una pregunta" "hasQuestion": "Roo tiene una pregunta"
}, },

View file

@ -826,6 +826,10 @@
"refreshSuccess": "Herramientas actualizadas correctamente", "refreshSuccess": "Herramientas actualizadas correctamente",
"refreshError": "Error al actualizar las herramientas", "refreshError": "Error al actualizar las herramientas",
"toolParameters": "Parámetros" "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": { "promptCaching": {

View file

@ -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.", "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" "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": { "questions": {
"hasQuestion": "Roo a une question" "hasQuestion": "Roo a une question"
}, },

View file

@ -826,6 +826,10 @@
"refreshSuccess": "Outils actualisés avec succès", "refreshSuccess": "Outils actualisés avec succès",
"refreshError": "Échec de l'actualisation des outils", "refreshError": "Échec de l'actualisation des outils",
"toolParameters": "Paramètres" "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": { "promptCaching": {

View file

@ -269,6 +269,36 @@
"completionInstructions": "उपकार्य पूर्ण! आप परिणामों की समीक्षा कर सकते हैं और सुधार या अगले चरण सुझा सकते हैं। यदि सब कुछ ठीक लगता है, तो मुख्य कार्य को परिणाम वापस करने के लिए पुष्टि करें।", "completionInstructions": "उपकार्य पूर्ण! आप परिणामों की समीक्षा कर सकते हैं और सुधार या अगले चरण सुझा सकते हैं। यदि सब कुछ ठीक लगता है, तो मुख्य कार्य को परिणाम वापस करने के लिए पुष्टि करें।",
"goToSubtask": "कार्य देखें" "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": { "questions": {
"hasQuestion": "Roo का एक प्रश्न है" "hasQuestion": "Roo का एक प्रश्न है"
}, },

View file

@ -826,6 +826,10 @@
"refreshSuccess": "टूल्स सफलतापूर्वक रिफ्रेश हुए", "refreshSuccess": "टूल्स सफलतापूर्वक रिफ्रेश हुए",
"refreshError": "टूल्स रिफ्रेश करने में विफल", "refreshError": "टूल्स रिफ्रेश करने में विफल",
"toolParameters": "पैरामीटर्स" "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": { "promptCaching": {

View file

@ -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.", "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" "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": { "questions": {
"hasQuestion": "Roo punya pertanyaan" "hasQuestion": "Roo punya pertanyaan"
}, },

View file

@ -826,6 +826,10 @@
"refreshSuccess": "Tool berhasil direfresh", "refreshSuccess": "Tool berhasil direfresh",
"refreshError": "Gagal merefresh tool", "refreshError": "Gagal merefresh tool",
"toolParameters": "Parameter" "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": { "promptCaching": {

View file

@ -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.", "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à" "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": { "questions": {
"hasQuestion": "Roo ha una domanda" "hasQuestion": "Roo ha una domanda"
}, },

View file

@ -826,6 +826,10 @@
"refreshSuccess": "Strumenti aggiornati con successo", "refreshSuccess": "Strumenti aggiornati con successo",
"refreshError": "Impossibile aggiornare gli strumenti", "refreshError": "Impossibile aggiornare gli strumenti",
"toolParameters": "Parametri" "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": { "promptCaching": {

View file

@ -269,6 +269,36 @@
"completionInstructions": "サブタスク完了!結果を確認し、修正や次のステップを提案できます。問題なければ、親タスクに結果を返すために確認してください。", "completionInstructions": "サブタスク完了!結果を確認し、修正や次のステップを提案できます。問題なければ、親タスクに結果を返すために確認してください。",
"goToSubtask": "タスクを表示" "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": { "questions": {
"hasQuestion": "Rooは質問があります" "hasQuestion": "Rooは質問があります"
}, },

View file

@ -826,6 +826,10 @@
"refreshSuccess": "ツールが正常に更新されました", "refreshSuccess": "ツールが正常に更新されました",
"refreshError": "ツールの更新に失敗しました", "refreshError": "ツールの更新に失敗しました",
"toolParameters": "パラメーター" "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": { "promptCaching": {

View file

@ -269,6 +269,36 @@
"completionInstructions": "하위 작업 완료! 결과를 검토하고 수정 사항이나 다음 단계를 제안할 수 있습니다. 모든 것이 괜찮아 보이면, 부모 작업에 결과를 반환하기 위해 확인해주세요.", "completionInstructions": "하위 작업 완료! 결과를 검토하고 수정 사항이나 다음 단계를 제안할 수 있습니다. 모든 것이 괜찮아 보이면, 부모 작업에 결과를 반환하기 위해 확인해주세요.",
"goToSubtask": "작업 보기" "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": { "questions": {
"hasQuestion": "Roo에게 질문이 있습니다" "hasQuestion": "Roo에게 질문이 있습니다"
}, },

View file

@ -826,6 +826,10 @@
"refreshSuccess": "도구가 성공적으로 새로고침되었습니다", "refreshSuccess": "도구가 성공적으로 새로고침되었습니다",
"refreshError": "도구 새로고침에 실패했습니다", "refreshError": "도구 새로고침에 실패했습니다",
"toolParameters": "매개변수" "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": { "promptCaching": {

View file

@ -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.", "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" "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": { "questions": {
"hasQuestion": "Roo heeft een vraag" "hasQuestion": "Roo heeft een vraag"
}, },

View file

@ -826,6 +826,10 @@
"refreshSuccess": "Tools succesvol vernieuwd", "refreshSuccess": "Tools succesvol vernieuwd",
"refreshError": "Fout bij vernieuwen van tools", "refreshError": "Fout bij vernieuwen van tools",
"toolParameters": "Parameters" "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": { "promptCaching": {

View file

@ -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.", "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" "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": { "questions": {
"hasQuestion": "Roo ma pytanie" "hasQuestion": "Roo ma pytanie"
}, },

View file

@ -826,6 +826,10 @@
"refreshSuccess": "Narzędzia odświeżone pomyślnie", "refreshSuccess": "Narzędzia odświeżone pomyślnie",
"refreshError": "Nie udało się odświeżyć narzędzi", "refreshError": "Nie udało się odświeżyć narzędzi",
"toolParameters": "Parametry" "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": { "promptCaching": {

View file

@ -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.", "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" "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": { "questions": {
"hasQuestion": "Roo tem uma pergunta" "hasQuestion": "Roo tem uma pergunta"
}, },

View file

@ -826,6 +826,10 @@
"refreshSuccess": "Ferramentas atualizadas com sucesso", "refreshSuccess": "Ferramentas atualizadas com sucesso",
"refreshError": "Falha ao atualizar ferramentas", "refreshError": "Falha ao atualizar ferramentas",
"toolParameters": "Parâmetros" "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": { "promptCaching": {

View file

@ -265,6 +265,36 @@
"completionInstructions": "Подзадача завершена! Вы можете просмотреть результаты и предложить исправления или следующие шаги. Если всё в порядке, подтвердите для возврата результата в родительскую задачу.", "completionInstructions": "Подзадача завершена! Вы можете просмотреть результаты и предложить исправления или следующие шаги. Если всё в порядке, подтвердите для возврата результата в родительскую задачу.",
"goToSubtask": "Просмотреть задачу" "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": { "questions": {
"hasQuestion": "У Roo есть вопрос" "hasQuestion": "У Roo есть вопрос"
}, },

View file

@ -826,6 +826,10 @@
"refreshSuccess": "Инструменты успешно обновлены", "refreshSuccess": "Инструменты успешно обновлены",
"refreshError": "Не удалось обновить инструменты", "refreshError": "Не удалось обновить инструменты",
"toolParameters": "Параметры" "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": { "promptCaching": {

View file

@ -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.", "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" "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": { "questions": {
"hasQuestion": "Roo'nun bir sorusu var" "hasQuestion": "Roo'nun bir sorusu var"
}, },

View file

@ -826,6 +826,10 @@
"refreshSuccess": "Araçlar başarıyla yenilendi", "refreshSuccess": "Araçlar başarıyla yenilendi",
"refreshError": "Araçlar yenilenemedi", "refreshError": "Araçlar yenilenemedi",
"toolParameters": "Parametreler" "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": { "promptCaching": {

View file

@ -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.", "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ụ" "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": { "questions": {
"hasQuestion": "Roo có một câu hỏi" "hasQuestion": "Roo có một câu hỏi"
}, },

View file

@ -826,6 +826,10 @@
"refreshSuccess": "Làm mới công cụ thành công", "refreshSuccess": "Làm mới công cụ thành công",
"refreshError": "Không thể làm mới công cụ", "refreshError": "Không thể làm mới công cụ",
"toolParameters": "Thông số" "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": { "promptCaching": {

View file

@ -270,6 +270,36 @@
"completionInstructions": "子任务已完成!您可以查看结果并提出修改或下一步建议。如果一切正常,请确认以将结果返回给主任务。", "completionInstructions": "子任务已完成!您可以查看结果并提出修改或下一步建议。如果一切正常,请确认以将结果返回给主任务。",
"goToSubtask": "查看任务" "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": { "questions": {
"hasQuestion": "Roo有一个问题" "hasQuestion": "Roo有一个问题"
}, },

View file

@ -826,6 +826,10 @@
"refreshSuccess": "工具刷新成功", "refreshSuccess": "工具刷新成功",
"refreshError": "工具刷新失败", "refreshError": "工具刷新失败",
"toolParameters": "参数" "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": { "promptCaching": {

View file

@ -314,6 +314,36 @@
"completionInstructions": "子任務已完成!您可以檢閱結果並提出修正或後續步驟。如果一切順利,請確認以將結果回傳給主任務。", "completionInstructions": "子任務已完成!您可以檢閱結果並提出修正或後續步驟。如果一切順利,請確認以將結果回傳給主任務。",
"goToSubtask": "查看工作" "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": { "questions": {
"hasQuestion": "Roo 有一個問題" "hasQuestion": "Roo 有一個問題"
}, },

View file

@ -836,6 +836,10 @@
"refreshSuccess": "工具重新整理成功", "refreshSuccess": "工具重新整理成功",
"refreshError": "工具重新整理失敗", "refreshError": "工具重新整理失敗",
"toolParameters": "參數" "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": { "promptCaching": {