mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
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:
parent
ad0e33e2d9
commit
e73e20da7a
5 changed files with 278 additions and 9 deletions
|
|
@ -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>
|
||||
`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue