diff --git a/src/core/prompts/tools/new-task.ts b/src/core/prompts/tools/new-task.ts index 7301b7b422..85324dad3b 100644 --- a/src/core/prompts/tools/new-task.ts +++ b/src/core/prompts/tools/new-task.ts @@ -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: your-mode-slug-here Your initial instructions here +optional-config-slug-here -Example: +Examples: + +1. Basic usage (without config): code Implement a new feature for the application. + +2. With specific configuration: + +architect +Design the database schema for the new feature +accurate-model + ` } diff --git a/src/core/tools/__tests__/newTaskTool.spec.ts b/src/core/tools/__tests__/newTaskTool.spec.ts index 1dd79d6e98..ced02780dd 100644 --- a/src/core/tools/__tests__/newTaskTool.spec.ts +++ b/src/core/tools/__tests__/newTaskTool.spec.ts @@ -26,6 +26,7 @@ const mockInitClineWithTask = vi.fn<() => Promise>().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 }) diff --git a/src/core/tools/newTaskTool.ts b/src/core/tools/newTaskTool.ts index 46a1fe5d9b..a6d49632b2 100644 --- a/src/core/tools/newTaskTool.ts +++ b/src/core/tools/newTaskTool.ts @@ -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. diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 274060a19b..1895a47478 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -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")) } diff --git a/src/shared/tools.ts b/src/shared/tools.ts index 67972243fe..e3b11e1dd1 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -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, "mode" | "message">> + params: Partial, "mode" | "message" | "config">> } export interface SearchAndReplaceToolUse extends ToolUse {