Roo-Code/src/core/tools/NewTaskTool.ts
davincidreams be83bb6698 feat: port Claude Code CLI features and add parallel agent/worktree support
- Port session memory compaction, microcompact, hooks system, and
  prompt-too-long retry from the Claude Code CLI source
- Add spawn_parallel_tasks tool with serial queue drain fan-out pattern
- Add worktree isolation: new_task/spawn_parallel_tasks accept a worktree
  param; ClineProvider creates/cleans up git worktrees per child task
- Add completionPayload: attempt_completion result parsed as JSON and
  aggregated across parallel tasks for structured fan-in
- Fix workspacePath priority so explicit override always wins over parent
- Emit TaskSpawned event from delegateParentAndOpenChild
- Scope MCP hub to per-task workspacePath for worktree-aware mcp.json
- Fix microCompact bugs: touched flag, off-by-one threshold, keepRecent:0
- Fix lazy OutputChannel init in hooks executor to avoid test failures
- Fix pre-existing lint warnings across src, packages/cloud, webview-ui

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 23:50:56 -04:00

147 lines
4.3 KiB
TypeScript

import * as vscode from "vscode"
import { TodoItem } from "@roo-code/types"
import { Task } from "../task/Task"
import { getModeBySlug } from "../../shared/modes"
import { formatResponse } from "../prompts/responses"
import { t } from "../../i18n"
import { parseMarkdownChecklist } from "./UpdateTodoListTool"
import { Package } from "../../shared/package"
import { BaseTool, ToolCallbacks } from "./BaseTool"
import type { ToolUse } from "../../shared/tools"
interface NewTaskParams {
mode: string
message: string
todos?: string
worktree?: string
}
export class NewTaskTool extends BaseTool<"new_task"> {
readonly name = "new_task" as const
async execute(params: NewTaskParams, task: Task, callbacks: ToolCallbacks): Promise<void> {
const { mode, message, todos, worktree } = params
const { askApproval, handleError, pushToolResult } = callbacks
try {
// Validate required parameters.
if (!mode) {
task.consecutiveMistakeCount++
task.recordToolError("new_task")
task.didToolFailInCurrentTurn = true
pushToolResult(await task.sayAndCreateMissingParamError("new_task", "mode"))
return
}
if (!message) {
task.consecutiveMistakeCount++
task.recordToolError("new_task")
task.didToolFailInCurrentTurn = true
pushToolResult(await task.sayAndCreateMissingParamError("new_task", "message"))
return
}
// Get the VSCode setting for requiring todos.
const provider = task.providerRef.deref()
if (!provider) {
pushToolResult(formatResponse.toolError("Provider reference lost"))
return
}
const state = await provider.getState()
// Use Package.name (dynamic at build time) as the VSCode configuration namespace.
// Supports multiple extension variants (e.g., stable/nightly) without hardcoded strings.
const requireTodos = vscode.workspace
.getConfiguration(Package.name)
.get<boolean>("newTaskRequireTodos", false)
// Check if todos are required based on VSCode setting.
// Note: `undefined` means not provided, empty string is valid.
if (requireTodos && todos === undefined) {
task.consecutiveMistakeCount++
task.recordToolError("new_task")
task.didToolFailInCurrentTurn = true
pushToolResult(await task.sayAndCreateMissingParamError("new_task", "todos"))
return
}
// Parse todos if provided, otherwise use empty array
let todoItems: TodoItem[] = []
if (todos) {
try {
todoItems = parseMarkdownChecklist(todos)
} catch (error) {
task.consecutiveMistakeCount++
task.recordToolError("new_task")
task.didToolFailInCurrentTurn = true
pushToolResult(formatResponse.toolError("Invalid todos format: must be a markdown checklist"))
return
}
}
task.consecutiveMistakeCount = 0
// Un-escape one level of backslashes before '@' for hierarchical subtasks
// Un-escape one level: \\@ -> \@ (removes one backslash for hierarchical subtasks)
const unescapedMessage = message.replace(/\\\\@/g, "\\@")
// Verify the mode exists
const targetMode = getModeBySlug(mode, state?.customModes)
if (!targetMode) {
pushToolResult(formatResponse.toolError(`Invalid mode: ${mode}`))
return
}
const toolMessage = JSON.stringify({
tool: "newTask",
mode: targetMode.name,
content: message,
todos: todoItems,
})
const didApprove = await askApproval("tool", toolMessage)
if (!didApprove) {
return
}
// Delegate parent and open child as sole active task
const child = await (provider as any).delegateParentAndOpenChild({
parentTaskId: task.taskId,
message: unescapedMessage,
initialTodos: todoItems,
mode,
worktree: worktree || undefined,
})
// Reflect delegation in tool result (no pause/unpause, no wait)
pushToolResult(`Delegated to child task ${child.taskId}`)
return
} catch (error) {
await handleError("creating new task", error)
return
}
}
override async handlePartial(task: Task, block: ToolUse<"new_task">): Promise<void> {
const mode: string | undefined = block.params.mode
const message: string | undefined = block.params.message
const todos: string | undefined = block.params.todos
const partialMessage = JSON.stringify({
tool: "newTask",
mode: mode ?? "",
content: message ?? "",
todos: todos,
})
await task.ask("tool", partialMessage, block.partial).catch(() => {})
}
}
export const newTaskTool = new NewTaskTool()