import cloneDeep from "clone-deep" import { serializeError } from "serialize-error" import { Anthropic } from "@anthropic-ai/sdk" import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { defaultModeSlug, getModeBySlug } from "../../shared/modes" import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../shared/tools" import { Package } from "../../shared/package" import { fetchInstructionsTool } from "../tools/FetchInstructionsTool" import { listFilesTool } from "../tools/ListFilesTool" import { readFileTool } from "../tools/ReadFileTool" import { getSimpleReadFileToolDescription, simpleReadFileTool } from "../tools/simpleReadFileTool" import { shouldUseSingleFileRead, TOOL_PROTOCOL } from "@roo-code/types" import { writeToFileTool } from "../tools/WriteToFileTool" import { applyDiffTool } from "../tools/MultiApplyDiffTool" import { insertContentTool } from "../tools/InsertContentTool" import { listCodeDefinitionNamesTool } from "../tools/ListCodeDefinitionNamesTool" import { searchFilesTool } from "../tools/SearchFilesTool" import { browserActionTool } from "../tools/BrowserActionTool" import { executeCommandTool } from "../tools/ExecuteCommandTool" import { useMcpToolTool } from "../tools/UseMcpToolTool" import { accessMcpResourceTool } from "../tools/accessMcpResourceTool" import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool" import { switchModeTool } from "../tools/SwitchModeTool" import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/AttemptCompletionTool" import { newTaskTool } from "../tools/NewTaskTool" import { updateTodoListTool } from "../tools/UpdateTodoListTool" import { runSlashCommandTool } from "../tools/RunSlashCommandTool" import { generateImageTool } from "../tools/GenerateImageTool" import { formatResponse } from "../prompts/responses" import { validateToolUse } from "../tools/validateToolUse" import { Task } from "../task/Task" import { codebaseSearchTool } from "../tools/CodebaseSearchTool" import { experiments, EXPERIMENT_IDS } from "../../shared/experiments" import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool" import { isNativeProtocol } from "@roo-code/types" import { resolveToolProtocol } from "../../utils/resolveToolProtocol" /** * Processes and presents assistant message content to the user interface. * * This function is the core message handling system that: * - Sequentially processes content blocks from the assistant's response. * - Displays text content to the user. * - Executes tool use requests with appropriate user approval. * - Manages the flow of conversation by determining when to proceed to the next content block. * - Coordinates file system checkpointing for modified files. * - Controls the conversation state to determine when to continue to the next request. * * The function uses a locking mechanism to prevent concurrent execution and handles * partial content blocks during streaming. It's designed to work with the streaming * API response pattern, where content arrives incrementally and needs to be processed * as it becomes available. */ export async function presentAssistantMessage(cline: Task) { if (cline.abort) { throw new Error(`[Task#presentAssistantMessage] task ${cline.taskId}.${cline.instanceId} aborted`) } if (cline.presentAssistantMessageLocked) { cline.presentAssistantMessageHasPendingUpdates = true return } cline.presentAssistantMessageLocked = true cline.presentAssistantMessageHasPendingUpdates = false if (cline.currentStreamingContentIndex >= cline.assistantMessageContent.length) { // This may happen if the last content block was completed before // streaming could finish. If streaming is finished, and we're out of // bounds then this means we already presented/executed the last // content block and are ready to continue to next request. if (cline.didCompleteReadingStream) { cline.userMessageContentReady = true } cline.presentAssistantMessageLocked = false return } let block: any try { block = cloneDeep(cline.assistantMessageContent[cline.currentStreamingContentIndex]) // need to create copy bc while stream is updating the array, it could be updating the reference block properties too } catch (error) { console.error(`ERROR cloning block:`, error) console.error( `Block content:`, JSON.stringify(cline.assistantMessageContent[cline.currentStreamingContentIndex], null, 2), ) cline.presentAssistantMessageLocked = false return } switch (block.type) { case "mcp_tool_use": { // Handle native MCP tool calls (from mcp_serverName_toolName dynamic tools) // These are converted to the same execution path as use_mcp_tool but preserve // their original name in API history const mcpBlock = block as McpToolUse if (cline.didRejectTool) { // For native protocol, we must send a tool_result for every tool_use to avoid API errors const toolCallId = mcpBlock.id const errorMessage = !mcpBlock.partial ? `Skipping MCP tool ${mcpBlock.name} due to user rejecting a previous tool.` : `MCP tool ${mcpBlock.name} was interrupted and not executed due to user rejecting a previous tool.` if (toolCallId) { cline.userMessageContent.push({ type: "tool_result", tool_use_id: toolCallId, content: errorMessage, is_error: true, } as Anthropic.ToolResultBlockParam) } break } if (cline.didAlreadyUseTool) { const toolCallId = mcpBlock.id const errorMessage = `MCP tool [${mcpBlock.name}] was not executed because a tool has already been used in this message. Only one tool may be used per message.` if (toolCallId) { cline.userMessageContent.push({ type: "tool_result", tool_use_id: toolCallId, content: errorMessage, is_error: true, } as Anthropic.ToolResultBlockParam) } break } // Track if we've already pushed a tool result let hasToolResult = false const toolCallId = mcpBlock.id const toolProtocol = TOOL_PROTOCOL.NATIVE // MCP tools in native mode always use native protocol const pushToolResult = (content: ToolResponse) => { if (hasToolResult) { console.warn( `[presentAssistantMessage] Skipping duplicate tool_result for mcp_tool_use: ${toolCallId}`, ) return } let resultContent: string let imageBlocks: Anthropic.ImageBlockParam[] = [] if (typeof content === "string") { resultContent = content || "(tool did not return anything)" } else { const textBlocks = content.filter((item) => item.type === "text") imageBlocks = content.filter((item) => item.type === "image") as Anthropic.ImageBlockParam[] resultContent = textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") || "(tool did not return anything)" } if (toolCallId) { cline.userMessageContent.push({ type: "tool_result", tool_use_id: toolCallId, content: resultContent, } as Anthropic.ToolResultBlockParam) if (imageBlocks.length > 0) { cline.userMessageContent.push(...imageBlocks) } } hasToolResult = true cline.didAlreadyUseTool = true } const toolDescription = () => `[mcp_tool: ${mcpBlock.serverName}/${mcpBlock.toolName}]` const askApproval = async ( type: ClineAsk, partialMessage?: string, progressStatus?: ToolProgressStatus, isProtected?: boolean, ) => { const { response, text, images } = await cline.ask( type, partialMessage, false, progressStatus, isProtected || false, ) if (response !== "yesButtonClicked") { if (text) { await cline.say("user_feedback", text, images) pushToolResult( formatResponse.toolResult( formatResponse.toolDeniedWithFeedback(text, toolProtocol), images, ), ) } else { pushToolResult(formatResponse.toolDenied(toolProtocol)) } cline.didRejectTool = true return false } if (text) { await cline.say("user_feedback", text, images) pushToolResult( formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text, toolProtocol), images), ) } return true } const handleError = async (action: string, error: Error) => { const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}` await cline.say( "error", `Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`, ) pushToolResult(formatResponse.toolError(errorString, toolProtocol)) } if (!mcpBlock.partial) { cline.recordToolUsage("use_mcp_tool") // Record as use_mcp_tool for analytics TelemetryService.instance.captureToolUsage(cline.taskId, "use_mcp_tool", toolProtocol) } // Execute the MCP tool using the same handler as use_mcp_tool // Create a synthetic ToolUse block that the useMcpToolTool can handle const syntheticToolUse: ToolUse<"use_mcp_tool"> = { type: "tool_use", id: mcpBlock.id, name: "use_mcp_tool", params: { server_name: mcpBlock.serverName, tool_name: mcpBlock.toolName, arguments: JSON.stringify(mcpBlock.arguments), }, partial: mcpBlock.partial, nativeArgs: { server_name: mcpBlock.serverName, tool_name: mcpBlock.toolName, arguments: mcpBlock.arguments, }, } await useMcpToolTool.handle(cline, syntheticToolUse, { askApproval, handleError, pushToolResult, removeClosingTag: (tag, text) => text || "", toolProtocol, }) break } case "text": { if (cline.didRejectTool || cline.didAlreadyUseTool) { break } let content = block.content if (content) { // Have to do this for partial and complete since sending // content in thinking tags to markdown renderer will // automatically be removed. // Remove end substrings of (with optional line break // after) and (with optional line break before). // - Needs to be separate since we dont want to remove the line // break before the first tag. // - Needs to happen before the xml parsing below. content = content.replace(/\s?/g, "") content = content.replace(/\s?<\/thinking>/g, "") // Remove partial XML tag at the very end of the content (for // tool use and thinking tags), Prevents scrollview from // jumping when tags are automatically removed. const lastOpenBracketIndex = content.lastIndexOf("<") if (lastOpenBracketIndex !== -1) { const possibleTag = content.slice(lastOpenBracketIndex) // Check if there's a '>' after the last '<' (i.e., if the // tag is complete) (complete thinking and tool tags will // have been removed by now.) const hasCloseBracket = possibleTag.includes(">") if (!hasCloseBracket) { // Extract the potential tag name. let tagContent: string if (possibleTag.startsWith(" { switch (block.name) { case "execute_command": return `[${block.name} for '${block.params.command}']` case "read_file": // Check if this model should use the simplified description const modelId = cline.api.getModel().id if (shouldUseSingleFileRead(modelId)) { return getSimpleReadFileToolDescription(block.name, block.params) } else { // Prefer native typed args when available; fall back to legacy params // Check if nativeArgs exists (native protocol) if (block.nativeArgs) { return readFileTool.getReadFileToolDescription(block.name, block.nativeArgs) } return readFileTool.getReadFileToolDescription(block.name, block.params) } case "fetch_instructions": return `[${block.name} for '${block.params.task}']` case "write_to_file": return `[${block.name} for '${block.params.path}']` case "apply_diff": // Handle both legacy format and new multi-file format if (block.params.path) { return `[${block.name} for '${block.params.path}']` } else if (block.params.args) { // Try to extract first file path from args for display const match = block.params.args.match(/.*?([^<]+)<\/path>/s) if (match) { const firstPath = match[1] // Check if there are multiple files const fileCount = (block.params.args.match(//g) || []).length if (fileCount > 1) { return `[${block.name} for '${firstPath}' and ${fileCount - 1} more file${fileCount > 2 ? "s" : ""}]` } else { return `[${block.name} for '${firstPath}']` } } } return `[${block.name}]` case "search_files": return `[${block.name} for '${block.params.regex}'${ block.params.file_pattern ? ` in '${block.params.file_pattern}'` : "" }]` case "insert_content": return `[${block.name} for '${block.params.path}']` case "list_files": return `[${block.name} for '${block.params.path}']` case "list_code_definition_names": return `[${block.name} for '${block.params.path}']` case "browser_action": return `[${block.name} for '${block.params.action}']` case "use_mcp_tool": return `[${block.name} for '${block.params.server_name}']` case "access_mcp_resource": return `[${block.name} for '${block.params.server_name}']` case "ask_followup_question": return `[${block.name} for '${block.params.question}']` case "attempt_completion": return `[${block.name}]` case "switch_mode": return `[${block.name} to '${block.params.mode_slug}'${block.params.reason ? ` because: ${block.params.reason}` : ""}]` case "codebase_search": // Add case for the new tool return `[${block.name} for '${block.params.query}']` case "update_todo_list": return `[${block.name}]` case "new_task": { const mode = block.params.mode ?? defaultModeSlug const message = block.params.message ?? "(no message)" const modeName = getModeBySlug(mode, customModes)?.name ?? mode return `[${block.name} in ${modeName} mode: '${message}']` } case "run_slash_command": return `[${block.name} for '${block.params.command}'${block.params.args ? ` with args: ${block.params.args}` : ""}]` case "generate_image": return `[${block.name} for '${block.params.path}']` default: return `[${block.name}]` } } if (cline.didRejectTool) { // Ignore any tool content after user has rejected tool once. // For native protocol, we must send a tool_result for every tool_use to avoid API errors const toolCallId = block.id const errorMessage = !block.partial ? `Skipping tool ${toolDescription()} due to user rejecting a previous tool.` : `Tool ${toolDescription()} was interrupted and not executed due to user rejecting a previous tool.` if (toolCallId) { // Native protocol: MUST send tool_result for every tool_use cline.userMessageContent.push({ type: "tool_result", tool_use_id: toolCallId, content: errorMessage, is_error: true, } as Anthropic.ToolResultBlockParam) } else { // XML protocol: send as text cline.userMessageContent.push({ type: "text", text: errorMessage, }) } break } if (cline.didAlreadyUseTool) { // Ignore any content after a tool has already been used. // For native protocol, we must send a tool_result for every tool_use to avoid API errors const toolCallId = block.id const errorMessage = `Tool [${block.name}] was not executed because a tool has already been used in this message. Only one tool may be used per message. You must assess the first tool's result before proceeding to use the next tool.` if (toolCallId) { // Native protocol: MUST send tool_result for every tool_use cline.userMessageContent.push({ type: "tool_result", tool_use_id: toolCallId, content: errorMessage, is_error: true, } as Anthropic.ToolResultBlockParam) } else { // XML protocol: send as text cline.userMessageContent.push({ type: "text", text: errorMessage, }) } break } // Track if we've already pushed a tool result for this tool call (native protocol only) let hasToolResult = false // Determine protocol by checking if this tool call has an ID. // Native protocol tool calls ALWAYS have an ID (set when parsed from tool_call chunks). // XML protocol tool calls NEVER have an ID (parsed from XML text). const toolCallId = (block as any).id const toolProtocol = toolCallId ? TOOL_PROTOCOL.NATIVE : TOOL_PROTOCOL.XML // Check experimental setting for multiple native tool calls const provider = cline.providerRef.deref() const state = await provider?.getState() const isMultipleNativeToolCallsEnabled = experiments.isEnabled( state?.experiments ?? {}, EXPERIMENT_IDS.MULTIPLE_NATIVE_TOOL_CALLS, ) const pushToolResult = (content: ToolResponse) => { if (toolProtocol === TOOL_PROTOCOL.NATIVE) { // For native protocol, only allow ONE tool_result per tool call if (hasToolResult) { console.warn( `[presentAssistantMessage] Skipping duplicate tool_result for tool_use_id: ${toolCallId}`, ) return } // For native protocol, tool_result content must be a string // Images are added as separate blocks in the user message let resultContent: string let imageBlocks: Anthropic.ImageBlockParam[] = [] if (typeof content === "string") { resultContent = content || "(tool did not return anything)" } else { // Separate text and image blocks const textBlocks = content.filter((item) => item.type === "text") imageBlocks = content.filter((item) => item.type === "image") as Anthropic.ImageBlockParam[] // Convert text blocks to string for tool_result resultContent = textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") || "(tool did not return anything)" } // Add tool_result with text content only cline.userMessageContent.push({ type: "tool_result", tool_use_id: toolCallId, content: resultContent, } as Anthropic.ToolResultBlockParam) // Add image blocks separately after tool_result if (imageBlocks.length > 0) { cline.userMessageContent.push(...imageBlocks) } hasToolResult = true } else { // For XML protocol, add as text blocks (legacy behavior) cline.userMessageContent.push({ type: "text", text: `${toolDescription()} Result:` }) if (typeof content === "string") { cline.userMessageContent.push({ type: "text", text: content || "(tool did not return anything)", }) } else { cline.userMessageContent.push(...content) } } // For XML protocol: Only one tool per message is allowed // For native protocol with experimental flag enabled: Multiple tools can be executed in sequence // For native protocol with experimental flag disabled: Single tool per message (default safe behavior) if (toolProtocol === TOOL_PROTOCOL.XML) { // Once a tool result has been collected, ignore all other tool // uses since we should only ever present one tool result per // message (XML protocol only). cline.didAlreadyUseTool = true } else if (toolProtocol === TOOL_PROTOCOL.NATIVE && !isMultipleNativeToolCallsEnabled) { // For native protocol with experimental flag disabled, enforce single tool per message cline.didAlreadyUseTool = true } // If toolProtocol is NATIVE and isMultipleNativeToolCallsEnabled is true, // allow multiple tool calls in sequence (don't set didAlreadyUseTool) } const askApproval = async ( type: ClineAsk, partialMessage?: string, progressStatus?: ToolProgressStatus, isProtected?: boolean, ) => { const { response, text, images } = await cline.ask( type, partialMessage, false, progressStatus, isProtected || false, ) if (response !== "yesButtonClicked") { // Handle both messageResponse and noButtonClicked with text. if (text) { await cline.say("user_feedback", text, images) pushToolResult( formatResponse.toolResult( formatResponse.toolDeniedWithFeedback(text, toolProtocol), images, ), ) } else { pushToolResult(formatResponse.toolDenied(toolProtocol)) } cline.didRejectTool = true return false } // Handle yesButtonClicked with text. if (text) { await cline.say("user_feedback", text, images) pushToolResult( formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text, toolProtocol), images), ) } return true } const askFinishSubTaskApproval = async () => { // Ask the user to approve this task has completed, and he has // reviewed it, and we can declare task is finished and return // control to the parent task to continue running the rest of // the sub-tasks. const toolMessage = JSON.stringify({ tool: "finishTask" }) return await askApproval("tool", toolMessage) } const handleError = async (action: string, error: Error) => { const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}` await cline.say( "error", `Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`, ) pushToolResult(formatResponse.toolError(errorString, toolProtocol)) } // If block is partial, remove partial closing tag so its not // presented to user. const removeClosingTag = (tag: ToolParamName, text?: string): string => { if (!block.partial) { return text || "" } if (!text) { return "" } // This regex dynamically constructs a pattern to match the // closing tag: // - Optionally matches whitespace before the tag. // - Matches '<' or ' `(?:${char})?`) .join("")}$`, "g", ) return text.replace(tagRegex, "") } // Keep browser open during an active session so other tools can run. // Session is active if we've seen any browser_action_result and the last browser_action is not "close". try { const messages = cline.clineMessages || [] const hasStarted = messages.some((m: any) => m.say === "browser_action_result") let isClosed = false for (let i = messages.length - 1; i >= 0; i--) { const m = messages[i] if (m.say === "browser_action") { try { const act = JSON.parse(m.text || "{}") isClosed = act.action === "close" } catch {} break } } const sessionActive = hasStarted && !isClosed // Only auto-close when no active browser session is present, and this isn't a browser_action if (!sessionActive && block.name !== "browser_action") { await cline.browserSession.closeBrowser() } } catch { // On any unexpected error, fall back to conservative behavior if (block.name !== "browser_action") { await cline.browserSession.closeBrowser() } } if (!block.partial) { cline.recordToolUsage(block.name) TelemetryService.instance.captureToolUsage(cline.taskId, block.name, toolProtocol) } // Validate tool use before execution. const { mode, customModes } = (await cline.providerRef.deref()?.getState()) ?? {} try { validateToolUse( block.name as ToolName, mode ?? defaultModeSlug, customModes ?? [], { apply_diff: cline.diffEnabled }, block.params, ) } catch (error) { cline.consecutiveMistakeCount++ pushToolResult(formatResponse.toolError(error.message, toolProtocol)) break } // Check for identical consecutive tool calls. if (!block.partial) { // Use the detector to check for repetition, passing the ToolUse // block directly. const repetitionCheck = cline.toolRepetitionDetector.check(block) // If execution is not allowed, notify user and break. if (!repetitionCheck.allowExecution && repetitionCheck.askUser) { // Handle repetition similar to mistake_limit_reached pattern. const { response, text, images } = await cline.ask( repetitionCheck.askUser.messageKey as ClineAsk, repetitionCheck.askUser.messageDetail.replace("{toolName}", block.name), ) if (response === "messageResponse") { // Add user feedback to userContent. cline.userMessageContent.push( { type: "text" as const, text: `Tool repetition limit reached. User feedback: ${text}`, }, ...formatResponse.imageBlocks(images), ) // Add user feedback to chat. await cline.say("user_feedback", text, images) // Track tool repetition in telemetry. TelemetryService.instance.captureConsecutiveMistakeError(cline.taskId) } // Return tool result message about the repetition pushToolResult( formatResponse.toolError( `Tool call repetition limit reached for ${block.name}. Please try a different approach.`, toolProtocol, ), ) break } } switch (block.name) { case "write_to_file": await checkpointSaveAndMark(cline) await writeToFileTool.handle(cline, block as ToolUse<"write_to_file">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break case "update_todo_list": await updateTodoListTool.handle(cline, block as ToolUse<"update_todo_list">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break case "apply_diff": { await checkpointSaveAndMark(cline) // Check if this tool call came from native protocol by checking for ID // Native calls always have IDs, XML calls never do if (toolProtocol === TOOL_PROTOCOL.NATIVE) { await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break } // Get the provider and state to check experiment settings const provider = cline.providerRef.deref() let isMultiFileApplyDiffEnabled = false if (provider) { const state = await provider.getState() isMultiFileApplyDiffEnabled = experiments.isEnabled( state.experiments ?? {}, EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF, ) } if (isMultiFileApplyDiffEnabled) { await applyDiffTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag) } else { await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) } break } case "insert_content": await checkpointSaveAndMark(cline) await insertContentTool.handle(cline, block as ToolUse<"insert_content">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break case "read_file": // Check if this model should use the simplified single-file read tool const modelId = cline.api.getModel().id if (shouldUseSingleFileRead(modelId)) { await simpleReadFileTool( cline, block, askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, ) } else { // Type assertion is safe here because we're in the "read_file" case await readFileTool.handle(cline, block as ToolUse<"read_file">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) } break case "fetch_instructions": await fetchInstructionsTool.handle(cline, block as ToolUse<"fetch_instructions">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break case "list_files": await listFilesTool.handle(cline, block as ToolUse<"list_files">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break case "codebase_search": await codebaseSearchTool.handle(cline, block as ToolUse<"codebase_search">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break case "list_code_definition_names": await listCodeDefinitionNamesTool.handle(cline, block as ToolUse<"list_code_definition_names">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break case "search_files": await searchFilesTool.handle(cline, block as ToolUse<"search_files">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break case "browser_action": await browserActionTool( cline, block as ToolUse<"browser_action">, askApproval, handleError, pushToolResult, removeClosingTag, ) break case "execute_command": await executeCommandTool.handle(cline, block as ToolUse<"execute_command">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break case "use_mcp_tool": await useMcpToolTool.handle(cline, block as ToolUse<"use_mcp_tool">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break case "access_mcp_resource": await accessMcpResourceTool.handle(cline, block as ToolUse<"access_mcp_resource">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break case "ask_followup_question": await askFollowupQuestionTool.handle(cline, block as ToolUse<"ask_followup_question">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break case "switch_mode": await switchModeTool.handle(cline, block as ToolUse<"switch_mode">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break case "new_task": await newTaskTool.handle(cline, block as ToolUse<"new_task">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break case "attempt_completion": { const completionCallbacks: AttemptCompletionCallbacks = { askApproval, handleError, pushToolResult, removeClosingTag, askFinishSubTaskApproval, toolDescription, toolProtocol, } await attemptCompletionTool.handle( cline, block as ToolUse<"attempt_completion">, completionCallbacks, ) break } case "run_slash_command": await runSlashCommandTool.handle(cline, block as ToolUse<"run_slash_command">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break case "generate_image": await checkpointSaveAndMark(cline) await generateImageTool.handle(cline, block as ToolUse<"generate_image">, { askApproval, handleError, pushToolResult, removeClosingTag, toolProtocol, }) break } break } // Seeing out of bounds is fine, it means that the next too call is being // built up and ready to add to assistantMessageContent to present. // When you see the UI inactive during this, it means that a tool is // breaking without presenting any UI. For example the write_to_file tool // was breaking when relpath was undefined, and for invalid relpath it never // presented UI. // This needs to be placed here, if not then calling // cline.presentAssistantMessage below would fail (sometimes) since it's // locked. cline.presentAssistantMessageLocked = false // NOTE: When tool is rejected, iterator stream is interrupted and it waits // for `userMessageContentReady` to be true. Future calls to present will // skip execution since `didRejectTool` and iterate until `contentIndex` is // set to message length and it sets userMessageContentReady to true itself // (instead of preemptively doing it in iterator). if (!block.partial || cline.didRejectTool || cline.didAlreadyUseTool) { // Block is finished streaming and executing. if (cline.currentStreamingContentIndex === cline.assistantMessageContent.length - 1) { // It's okay that we increment if !didCompleteReadingStream, it'll // just return because out of bounds and as streaming continues it // will call `presentAssitantMessage` if a new block is ready. If // streaming is finished then we set `userMessageContentReady` to // true when out of bounds. This gracefully allows the stream to // continue on and all potential content blocks be presented. // Last block is complete and it is finished executing cline.userMessageContentReady = true // Will allow `pWaitFor` to continue. } // Call next block if it exists (if not then read stream will call it // when it's ready). // Need to increment regardless, so when read stream calls this function // again it will be streaming the next block. cline.currentStreamingContentIndex++ if (cline.currentStreamingContentIndex < cline.assistantMessageContent.length) { // There are already more content blocks to stream, so we'll call // this function ourselves. presentAssistantMessage(cline) return } else { // CRITICAL FIX: If we're out of bounds and the stream is complete, set userMessageContentReady // This handles the case where assistantMessageContent is empty or becomes empty after processing if (cline.didCompleteReadingStream) { cline.userMessageContentReady = true } } } // Block is partial, but the read stream may have finished. if (cline.presentAssistantMessageHasPendingUpdates) { presentAssistantMessage(cline) } } /** * save checkpoint and mark done in the current streaming task. * @param task The Task instance to checkpoint save and mark. * @returns */ async function checkpointSaveAndMark(task: Task) { if (task.currentStreamingDidCheckpoint) { return } try { await task.checkpointSave(true) task.currentStreamingDidCheckpoint = true } catch (error) { console.error(`[Task#presentAssistantMessage] Error saving checkpoint: ${error.message}`, error) } }