diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index e0c332d16f..e7a5ed047e 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -225,8 +225,14 @@ export class Task extends EventEmitter implements TaskLike { private askResponse?: ClineAskResponse private askResponseText?: string private askResponseImages?: string[] + private askResponseValues?: Record public lastMessageTs?: number + // Getter for askResponseValues to allow tools to access it + get getAskResponseValues(): Record | undefined { + return this.askResponseValues + } + // Tool Use consecutiveMistakeCount: number = 0 consecutiveMistakeLimit: number @@ -742,10 +748,19 @@ export class Task extends EventEmitter implements TaskLike { this.handleWebviewAskResponse("messageResponse", text, images) } - handleWebviewAskResponse(askResponse: ClineAskResponse, text?: string, images?: string[]) { + handleWebviewAskResponse( + askResponse: ClineAskResponse, + text?: string, + images?: string[], + values?: Record, + ) { this.askResponse = askResponse this.askResponseText = text this.askResponseImages = images + // Store values for later use if needed + if (values) { + this.askResponseValues = values + } } async handleTerminalOperation(terminalOperation: "continue" | "abort") { diff --git a/src/core/tools/__tests__/newTaskTool.spec.ts b/src/core/tools/__tests__/newTaskTool.spec.ts index 1dd79d6e98..b932bf8923 100644 --- a/src/core/tools/__tests__/newTaskTool.spec.ts +++ b/src/core/tools/__tests__/newTaskTool.spec.ts @@ -36,6 +36,7 @@ const mockCline = { consecutiveMistakeCount: 0, isPaused: false, pausedModeSlug: "ask", + getAskResponseValues: undefined as Record | undefined, providerRef: { deref: vi.fn(() => ({ getState: vi.fn(() => ({ customModes: [], mode: "ask" })), @@ -184,4 +185,143 @@ describe("newTaskTool", () => { }) // Add more tests for error handling (missing params, invalid mode, approval denied) if needed + + it("should use user-selected mode when provided in askResponseValues", async () => { + const block: ToolUse = { + type: "tool_use", + name: "new_task", + params: { + mode: "code", + message: "Create a new feature", + }, + partial: false, + } + + // Mock user selecting a different mode + mockCline.getAskResponseValues = { selectedMode: "architect" } + + // Mock the architect mode + vi.mocked(getModeBySlug).mockImplementation((slug) => { + if (slug === "architect") { + return { + slug: "architect", + name: "Architect Mode", + roleDefinition: "Architecture role definition", + groups: ["command", "read"], + } + } + return { + slug: "code", + name: "Code Mode", + roleDefinition: "Test role definition", + groups: ["command", "read", "edit"], + } + }) + + const mockHandleModeSwitch = vi.fn() + mockCline.providerRef.deref = vi.fn(() => ({ + getState: vi.fn(() => ({ customModes: [], mode: "ask" })), + handleModeSwitch: mockHandleModeSwitch, + initClineWithTask: mockInitClineWithTask, + })) + + await newTaskTool( + mockCline as any, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + // Verify the mode switch was called with the user-selected mode + expect(mockHandleModeSwitch).toHaveBeenCalledWith("architect") + + // Verify the success message includes the correct mode name + expect(mockPushToolResult).toHaveBeenCalledWith( + expect.stringContaining("Successfully created new task in Architect Mode"), + ) + }) + + it("should use original mode when no user selection is provided", async () => { + const block: ToolUse = { + type: "tool_use", + name: "new_task", + params: { + mode: "code", + message: "Create a new feature", + }, + partial: false, + } + + // No user selection + mockCline.getAskResponseValues = undefined + + const mockHandleModeSwitch = vi.fn() + mockCline.providerRef.deref = vi.fn(() => ({ + getState: vi.fn(() => ({ customModes: [], mode: "ask" })), + handleModeSwitch: mockHandleModeSwitch, + initClineWithTask: mockInitClineWithTask, + })) + + await newTaskTool( + mockCline as any, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + // Verify the mode switch was called with the original mode + expect(mockHandleModeSwitch).toHaveBeenCalledWith("code") + + // Verify the success message includes the correct mode name + expect(mockPushToolResult).toHaveBeenCalledWith( + expect.stringContaining("Successfully created new task in Code Mode"), + ) + }) + + it("should handle invalid user-selected mode gracefully", async () => { + const block: ToolUse = { + type: "tool_use", + name: "new_task", + params: { + mode: "code", + message: "Create a new feature", + }, + partial: false, + } + + // Mock user selecting an invalid mode + mockCline.getAskResponseValues = { selectedMode: "invalid-mode" } + + // Mock getModeBySlug to return undefined for invalid mode + vi.mocked(getModeBySlug).mockImplementation((slug) => { + if (slug === "invalid-mode") { + return undefined + } + return { + slug: "code", + name: "Code Mode", + roleDefinition: "Test role definition", + groups: ["command", "read", "edit"], + } + }) + + await newTaskTool( + mockCline as any, + block, + mockAskApproval, + mockHandleError, + mockPushToolResult, + mockRemoveClosingTag, + ) + + // Verify error was pushed + expect(mockPushToolResult).toHaveBeenCalledWith("Tool Error: Invalid mode: invalid-mode") + + // Verify no task was created + expect(mockInitClineWithTask).not.toHaveBeenCalled() + }) }) diff --git a/src/core/tools/newTaskTool.ts b/src/core/tools/newTaskTool.ts index 46a1fe5d9b..e495bba343 100644 --- a/src/core/tools/newTaskTool.ts +++ b/src/core/tools/newTaskTool.ts @@ -75,6 +75,17 @@ export async function newTaskTool( return } + // Check if user selected a different mode during approval + const selectedMode = cline.getAskResponseValues?.selectedMode as string | undefined + const finalMode = selectedMode || mode + + // Verify the final mode exists + const finalTargetMode = getModeBySlug(finalMode, (await provider.getState())?.customModes) + if (!finalTargetMode) { + pushToolResult(formatResponse.toolError(`Invalid mode: ${finalMode}`)) + return + } + if (cline.enableCheckpoints) { cline.checkpointSave(true) } @@ -89,15 +100,17 @@ export async function newTaskTool( return } - // Now switch the newly created task to the desired mode - await provider.handleModeSwitch(mode) + // Now switch the newly created task to the desired mode (using the final mode) + await provider.handleModeSwitch(finalMode) // Delay to allow mode change to take effect await delay(500) cline.emit(RooCodeEventName.TaskSpawned, newCline.taskId) - pushToolResult(`Successfully created new task in ${targetMode.name} mode with message: ${unescapedMessage}`) + pushToolResult( + `Successfully created new task in ${finalTargetMode.name} mode with message: ${unescapedMessage}`, + ) // Set the isPaused flag to true so the parent // task can wait for the sub-task to finish. diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 4fa921f443..5cd8f4823f 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -33,6 +33,7 @@ import MarkdownBlock from "../common/MarkdownBlock" import { ReasoningBlock } from "./ReasoningBlock" import Thumbnails from "../common/Thumbnails" import McpResourceRow from "../mcp/McpResourceRow" +import ModeSelector from "./ModeSelector" import { Mention } from "./Mention" import { CheckpointSaved } from "./checkpoints/CheckpointSaved" @@ -60,6 +61,7 @@ interface ChatRowProps { onFollowUpUnmount?: () => void isFollowUpAnswered?: boolean editable?: boolean + onNewTaskModeChange?: (mode: string) => void } // eslint-disable-next-line @typescript-eslint/no-empty-object-type @@ -112,9 +114,10 @@ export const ChatRowContent = ({ onBatchFileResponse, isFollowUpAnswered, editable, + onNewTaskModeChange, }: ChatRowContentProps) => { const { t } = useTranslation() - const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode } = useExtensionState() + const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, customModes, customModePrompts } = useExtensionState() const [reasoningCollapsed, setReasoningCollapsed] = useState(true) const [isDiffErrorExpanded, setIsDiffErrorExpanded] = useState(false) const [showCopySuccess, setShowCopySuccess] = useState(false) @@ -122,6 +125,7 @@ export const ChatRowContent = ({ const [editedContent, setEditedContent] = useState("") const [editMode, setEditMode] = useState(mode || "code") const [editImages, setEditImages] = useState([]) + const [selectedNewTaskMode, setSelectedNewTaskMode] = useState(null) const { copyWithFeedback } = useCopyToClipboard() // Handle message events for image selection during edit mode @@ -765,16 +769,39 @@ export const ChatRowContent = ({ ) case "newTask": + // Use the selected mode if available, otherwise use the tool's mode + const effectiveMode = selectedNewTaskMode || tool.mode + return ( <>
{toolIcon("tasklist")} - - {tool.mode} }} - values={{ mode: tool.mode }} - /> + + {t("chat:subtasks.wantsToCreate").split("{mode}")[0]} + {message.type === "ask" ? ( + { + setSelectedNewTaskMode(newMode) + // Update the tool data with the new mode + if (tool) { + tool.mode = newMode + } + // Notify parent component + onNewTaskModeChange?.(newMode) + }} + disabled={false} + title={t("chat:subtasks.selectMode")} + triggerClassName="inline-flex" + modeShortcutText="" + customModes={customModes} + customModePrompts={customModePrompts} + disableSearch={true} + /> + ) : ( + {effectiveMode} + )} + {t("chat:subtasks.wantsToCreate").split("{mode}")[1]}
(null) const userRespondedRef = useRef(false) const [currentFollowUpTs, setCurrentFollowUpTs] = useState(null) + const [selectedNewTaskModes, setSelectedNewTaskModes] = useState>({}) const clineAskRef = useRef(clineAsk) useEffect(() => { @@ -709,7 +710,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + (text?: string, images?: string[], additionalData?: any) => { // Mark that user has responded userRespondedRef.current = true @@ -730,12 +731,17 @@ const ChatViewComponent: React.ForwardRefRenderFunction { + setSelectedNewTaskModes((prev) => ({ ...prev, [messageOrGroup.ts]: mode })) + }} editable={ messageOrGroup.type === "ask" && messageOrGroup.ask === "tool" && @@ -1912,7 +1921,26 @@ const ChatViewComponent: React.ForwardRefRenderFunction handlePrimaryButtonClick(inputValue, selectedImages)}> + onClick={() => { + // Check if this is a newTask tool and we have a selected mode + let additionalData = undefined + if (lastMessage?.ask === "tool") { + try { + const tool = JSON.parse(lastMessage.text || "{}") + if ( + tool.tool === "newTask" && + selectedNewTaskModes[lastMessage.ts] + ) { + additionalData = { + selectedMode: selectedNewTaskModes[lastMessage.ts], + } + } + } catch (_e) { + // Ignore parse errors + } + } + handlePrimaryButtonClick(inputValue, selectedImages, additionalData) + }}> {primaryButtonText} diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 07bcd770d7..ff1865e03e 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -247,7 +247,8 @@ "completionContent": "Subtask Completed", "resultContent": "Subtask Results", "defaultResult": "Please continue to the next task.", - "completionInstructions": "Subtask completed! 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": "Subtask completed! 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.", + "selectMode": "Click to select a different mode for this subtask" }, "questions": { "hasQuestion": "Roo has a question:"