feat: add config parameter to new_task tool

- Add optional config parameter to new_task tool description
- Update newTaskTool.ts to handle config parameter validation
- Modify initClineWithTask to accept and apply config profiles
- Add comprehensive tests for new functionality
- Maintain backward compatibility for existing usage

Fixes #6839
This commit is contained in:
Roo Code 2025-08-08 08:50:34 +00:00
parent ad0e33e2d9
commit e73e20da7a
5 changed files with 278 additions and 9 deletions

View file

@ -2,22 +2,33 @@ import { ToolArgs } from "./types"
export function getNewTaskDescription(_args: ToolArgs): string {
return `## new_task
Description: This will let you create a new task instance in the chosen mode using your provided message.
Description: This will let you create a new task instance in the chosen mode using your provided message and optionally specify an API configuration profile to use.
Parameters:
- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
- message: (required) The initial user message or instructions for this new task.
- config: (optional) The slug/name of the API configuration profile to use for this task (e.g., "claude-3-5-sonnet", "gpt-4-debug", "fast-model"). If not specified, uses the default configuration for the mode.
Usage:
<new_task>
<mode>your-mode-slug-here</mode>
<message>Your initial instructions here</message>
<config>optional-config-slug-here</config>
</new_task>
Example:
Examples:
1. Basic usage (without config):
<new_task>
<mode>code</mode>
<message>Implement a new feature for the application.</message>
</new_task>
2. With specific configuration:
<new_task>
<mode>architect</mode>
<message>Design the database schema for the new feature</message>
<config>accurate-model</config>
</new_task>
`
}

View file

@ -26,6 +26,7 @@ const mockInitClineWithTask = vi.fn<() => Promise<MockClineInstance>>().mockReso
const mockEmit = vi.fn()
const mockRecordToolError = vi.fn()
const mockSayAndCreateMissingParamError = vi.fn()
const mockHasConfig = vi.fn()
// Mock the Cline instance and its methods/properties
const mockCline = {
@ -41,6 +42,9 @@ const mockCline = {
getState: vi.fn(() => ({ customModes: [], mode: "ask" })),
handleModeSwitch: vi.fn(),
initClineWithTask: mockInitClineWithTask,
providerSettingsManager: {
hasConfig: mockHasConfig,
},
})),
},
}
@ -63,6 +67,7 @@ describe("newTaskTool", () => {
}) // Default valid mode
mockCline.consecutiveMistakeCount = 0
mockCline.isPaused = false
mockHasConfig.mockResolvedValue(true) // Default to config exists
})
it("should correctly un-escape \\\\@ to \\@ in the message passed to the new task", async () => {
@ -93,6 +98,7 @@ describe("newTaskTool", () => {
"Review this: \\@file1.txt and also \\\\\\@file2.txt", // Unit Test Expectation: \\@ -> \@, \\\\@ -> \\\\@
undefined,
mockCline,
undefined, // No config parameter for this test
)
// Verify side effects
@ -126,6 +132,7 @@ describe("newTaskTool", () => {
"This is already unescaped: \\@file1.txt", // Expected: \@ remains \@
undefined,
mockCline,
undefined, // No config parameter for this test
)
})
@ -153,6 +160,7 @@ describe("newTaskTool", () => {
"A normal mention @file1.txt", // Expected: @ remains @
undefined,
mockCline,
undefined, // No config parameter for this test
)
})
@ -180,8 +188,188 @@ describe("newTaskTool", () => {
"Mix: @file0.txt, \\@file1.txt, \\@file2.txt, \\\\\\@file3.txt", // Unit Test Expectation: @->@, \@->\@, \\@->\@, \\\\@->\\\\@
undefined,
mockCline,
undefined, // No config parameter for this test
)
})
// Tests for the new config parameter functionality
describe("config parameter", () => {
it("should pass config parameter to initClineWithTask when valid config is provided", async () => {
const block: ToolUse = {
type: "tool_use",
name: "new_task",
params: {
mode: "code",
message: "Test message",
config: "fast-model",
},
partial: false,
}
mockHasConfig.mockResolvedValue(true)
await newTaskTool(
mockCline as any,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Verify hasConfig was called to validate the config
expect(mockHasConfig).toHaveBeenCalledWith("fast-model")
// Verify initClineWithTask was called with the config parameter
expect(mockInitClineWithTask).toHaveBeenCalledWith(
"Test message",
undefined,
mockCline,
"fast-model", // The config parameter should be passed
)
// Verify success message includes config name
expect(mockPushToolResult).toHaveBeenCalledWith(expect.stringContaining("configuration 'fast-model'"))
})
it("should continue without config when invalid config is provided", async () => {
const block: ToolUse = {
type: "tool_use",
name: "new_task",
params: {
mode: "code",
message: "Test message",
config: "non-existent-config",
},
partial: false,
}
mockHasConfig.mockResolvedValue(false)
await newTaskTool(
mockCline as any,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Verify hasConfig was called
expect(mockHasConfig).toHaveBeenCalledWith("non-existent-config")
// Verify error message was pushed
expect(mockPushToolResult).toHaveBeenCalledWith(
expect.stringContaining("Configuration profile 'non-existent-config' not found"),
)
// Verify initClineWithTask was called without the config parameter
expect(mockInitClineWithTask).toHaveBeenCalledWith(
"Test message",
undefined,
mockCline,
undefined, // No config should be passed
)
// Verify success message doesn't include config
expect(mockPushToolResult).toHaveBeenCalledWith(
expect.stringContaining("Successfully created new task in Code Mode mode with message: Test message"),
)
})
it("should work without config parameter (backward compatibility)", async () => {
const block: ToolUse = {
type: "tool_use",
name: "new_task",
params: {
mode: "code",
message: "Test message",
// No config parameter
},
partial: false,
}
await newTaskTool(
mockCline as any,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Verify hasConfig was NOT called
expect(mockHasConfig).not.toHaveBeenCalled()
// Verify initClineWithTask was called without config
expect(mockInitClineWithTask).toHaveBeenCalledWith(
"Test message",
undefined,
mockCline,
undefined, // No config parameter
)
// Verify success message doesn't include config
expect(mockPushToolResult).toHaveBeenCalledWith(
expect.stringContaining("Successfully created new task in Code Mode mode with message: Test message"),
)
})
it("should include config in approval message when config is provided", async () => {
const block: ToolUse = {
type: "tool_use",
name: "new_task",
params: {
mode: "code",
message: "Test message",
config: "accurate-model",
},
partial: false,
}
mockHasConfig.mockResolvedValue(true)
await newTaskTool(
mockCline as any,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Verify askApproval was called with a message containing the config
expect(mockAskApproval).toHaveBeenCalledWith("tool", expect.stringContaining('"config":"accurate-model"'))
})
it("should handle partial messages with config parameter", async () => {
const block: ToolUse = {
type: "tool_use",
name: "new_task",
params: {
mode: "code",
message: "Test message",
config: "fast-model",
},
partial: true,
}
await newTaskTool(
mockCline as any,
block,
mockAskApproval,
mockHandleError,
mockPushToolResult,
mockRemoveClosingTag,
)
// Verify ask was called with partial message including config
expect(mockCline.ask).toHaveBeenCalledWith("tool", expect.stringContaining('"config":"fast-model"'), true)
// Verify initClineWithTask was NOT called for partial message
expect(mockInitClineWithTask).not.toHaveBeenCalled()
})
})
// Add more tests for error handling (missing params, invalid mode, approval denied) if needed
})

View file

@ -18,6 +18,7 @@ export async function newTaskTool(
) {
const mode: string | undefined = block.params.mode
const message: string | undefined = block.params.message
const config: string | undefined = block.params.config
try {
if (block.partial) {
@ -25,6 +26,7 @@ export async function newTaskTool(
tool: "newTask",
mode: removeClosingTag("mode", mode),
content: removeClosingTag("message", message),
config: config ? removeClosingTag("config", config) : undefined,
})
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
@ -57,10 +59,33 @@ export async function newTaskTool(
return
}
// If a config was specified, verify it exists
let configName: string | undefined
if (config) {
const provider = cline.providerRef.deref()
if (!provider) {
return
}
// Check if the specified config exists
const hasConfig = await provider.providerSettingsManager.hasConfig(config)
if (!hasConfig) {
pushToolResult(
formatResponse.toolError(
`Configuration profile '${config}' not found. Using default configuration.`,
),
)
// Continue without the config rather than failing completely
} else {
configName = config
}
}
const toolMessage = JSON.stringify({
tool: "newTask",
mode: targetMode.name,
content: message,
...(configName && { config: configName }),
})
const didApprove = await askApproval("tool", toolMessage)
@ -83,7 +108,7 @@ export async function newTaskTool(
cline.pausedModeSlug = (await provider.getState()).mode ?? defaultModeSlug
// Create new task instance first (this preserves parent's current mode in its history)
const newCline = await provider.initClineWithTask(unescapedMessage, undefined, cline)
const newCline = await provider.initClineWithTask(unescapedMessage, undefined, cline, configName)
if (!newCline) {
pushToolResult(t("tools:newTask.errors.policy_restriction"))
return
@ -97,7 +122,10 @@ export async function newTaskTool(
cline.emit(RooCodeEventName.TaskSpawned, newCline.taskId)
pushToolResult(`Successfully created new task in ${targetMode.name} mode with message: ${unescapedMessage}`)
const successMessage = configName
? `Successfully created new task in ${targetMode.name} mode with configuration '${configName}' and message: ${unescapedMessage}`
: `Successfully created new task in ${targetMode.name} mode with message: ${unescapedMessage}`
pushToolResult(successMessage)
// Set the isPaused flag to true so the parent
// task can wait for the sub-task to finish.

View file

@ -638,14 +638,35 @@ export class ClineProvider
text?: string,
images?: string[],
parentTask?: Task,
options: Partial<
configNameOrOptions?:
| string
| Partial<
Pick<
TaskOptions,
| "enableDiff"
| "enableCheckpoints"
| "fuzzyMatchThreshold"
| "consecutiveMistakeLimit"
| "experiments"
>
>,
) {
// Handle both string (config name) and options object
let configName: string | undefined
let options: Partial<
Pick<
TaskOptions,
"enableDiff" | "enableCheckpoints" | "fuzzyMatchThreshold" | "consecutiveMistakeLimit" | "experiments"
>
> = {},
) {
const {
> = {}
if (typeof configNameOrOptions === "string") {
configName = configNameOrOptions
} else if (configNameOrOptions) {
options = configNameOrOptions
}
let {
apiConfiguration,
organizationAllowList,
diffEnabled: enableDiff,
@ -654,6 +675,26 @@ export class ClineProvider
experiments,
} = await this.getState()
// If a specific config was requested, load and apply it
if (configName) {
try {
const configProfile = await this.providerSettingsManager.getProfile({ name: configName })
// Use the configuration from the specified profile
apiConfiguration = configProfile
// Also use the diff and fuzzy match settings from the profile if available
if (configProfile.diffEnabled !== undefined) {
enableDiff = configProfile.diffEnabled
}
if (configProfile.fuzzyMatchThreshold !== undefined) {
fuzzyMatchThreshold = configProfile.fuzzyMatchThreshold
}
} catch (error) {
// Log error but continue with default config
this.log(`Failed to load config '${configName}' for new task: ${error}`)
// Continue with the current/default configuration
}
}
if (!ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList)) {
throw new OrganizationAllowListViolationError(t("common:errors.violated_organization_allowlist"))
}

View file

@ -65,6 +65,7 @@ export const toolParamNames = [
"query",
"args",
"todos",
"config",
] as const
export type ToolParamName = (typeof toolParamNames)[number]
@ -155,7 +156,7 @@ export interface SwitchModeToolUse extends ToolUse {
export interface NewTaskToolUse extends ToolUse {
name: "new_task"
params: Partial<Pick<Record<ToolParamName, string>, "mode" | "message">>
params: Partial<Pick<Record<ToolParamName, string>, "mode" | "message" | "config">>
}
export interface SearchAndReplaceToolUse extends ToolUse {