presentAssistantMessage.ts 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263
  1. import cloneDeep from "clone-deep"
  2. import { serializeError } from "serialize-error"
  3. import { Anthropic } from "@anthropic-ai/sdk"
  4. import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types"
  5. import { ConsecutiveMistakeError } from "@roo-code/types"
  6. import { TelemetryService } from "@roo-code/telemetry"
  7. import { customToolRegistry } from "@roo-code/core"
  8. import { t } from "../../i18n"
  9. import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
  10. import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../shared/tools"
  11. import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
  12. import { AskIgnoredError } from "../task/AskIgnoredError"
  13. import { Task } from "../task/Task"
  14. import { fetchInstructionsTool } from "../tools/FetchInstructionsTool"
  15. import { listFilesTool } from "../tools/ListFilesTool"
  16. import { readFileTool } from "../tools/ReadFileTool"
  17. import { TOOL_PROTOCOL } from "@roo-code/types"
  18. import { writeToFileTool } from "../tools/WriteToFileTool"
  19. import { applyDiffTool } from "../tools/MultiApplyDiffTool"
  20. import { searchAndReplaceTool } from "../tools/SearchAndReplaceTool"
  21. import { searchReplaceTool } from "../tools/SearchReplaceTool"
  22. import { editFileTool } from "../tools/EditFileTool"
  23. import { applyPatchTool } from "../tools/ApplyPatchTool"
  24. import { searchFilesTool } from "../tools/SearchFilesTool"
  25. import { browserActionTool } from "../tools/BrowserActionTool"
  26. import { executeCommandTool } from "../tools/ExecuteCommandTool"
  27. import { useMcpToolTool } from "../tools/UseMcpToolTool"
  28. import { accessMcpResourceTool } from "../tools/accessMcpResourceTool"
  29. import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool"
  30. import { switchModeTool } from "../tools/SwitchModeTool"
  31. import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/AttemptCompletionTool"
  32. import { newTaskTool } from "../tools/NewTaskTool"
  33. import { updateTodoListTool } from "../tools/UpdateTodoListTool"
  34. import { runSlashCommandTool } from "../tools/RunSlashCommandTool"
  35. import { generateImageTool } from "../tools/GenerateImageTool"
  36. import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
  37. import { validateToolUse } from "../tools/validateToolUse"
  38. import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
  39. import { formatResponse } from "../prompts/responses"
  40. /**
  41. * Processes and presents assistant message content to the user interface.
  42. *
  43. * This function is the core message handling system that:
  44. * - Sequentially processes content blocks from the assistant's response.
  45. * - Displays text content to the user.
  46. * - Executes tool use requests with appropriate user approval.
  47. * - Manages the flow of conversation by determining when to proceed to the next content block.
  48. * - Coordinates file system checkpointing for modified files.
  49. * - Controls the conversation state to determine when to continue to the next request.
  50. *
  51. * The function uses a locking mechanism to prevent concurrent execution and handles
  52. * partial content blocks during streaming. It's designed to work with the streaming
  53. * API response pattern, where content arrives incrementally and needs to be processed
  54. * as it becomes available.
  55. */
  56. export async function presentAssistantMessage(cline: Task) {
  57. if (cline.abort) {
  58. throw new Error(`[Task#presentAssistantMessage] task ${cline.taskId}.${cline.instanceId} aborted`)
  59. }
  60. if (cline.presentAssistantMessageLocked) {
  61. cline.presentAssistantMessageHasPendingUpdates = true
  62. return
  63. }
  64. cline.presentAssistantMessageLocked = true
  65. cline.presentAssistantMessageHasPendingUpdates = false
  66. if (cline.currentStreamingContentIndex >= cline.assistantMessageContent.length) {
  67. // This may happen if the last content block was completed before
  68. // streaming could finish. If streaming is finished, and we're out of
  69. // bounds then this means we already presented/executed the last
  70. // content block and are ready to continue to next request.
  71. if (cline.didCompleteReadingStream) {
  72. cline.userMessageContentReady = true
  73. }
  74. cline.presentAssistantMessageLocked = false
  75. return
  76. }
  77. let block: any
  78. try {
  79. 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
  80. } catch (error) {
  81. console.error(`ERROR cloning block:`, error)
  82. console.error(
  83. `Block content:`,
  84. JSON.stringify(cline.assistantMessageContent[cline.currentStreamingContentIndex], null, 2),
  85. )
  86. cline.presentAssistantMessageLocked = false
  87. return
  88. }
  89. switch (block.type) {
  90. case "mcp_tool_use": {
  91. // Handle native MCP tool calls (from mcp_serverName_toolName dynamic tools)
  92. // These are converted to the same execution path as use_mcp_tool but preserve
  93. // their original name in API history
  94. const mcpBlock = block as McpToolUse
  95. if (cline.didRejectTool) {
  96. // For native protocol, we must send a tool_result for every tool_use to avoid API errors
  97. const toolCallId = mcpBlock.id
  98. const errorMessage = !mcpBlock.partial
  99. ? `Skipping MCP tool ${mcpBlock.name} due to user rejecting a previous tool.`
  100. : `MCP tool ${mcpBlock.name} was interrupted and not executed due to user rejecting a previous tool.`
  101. if (toolCallId) {
  102. cline.pushToolResultToUserContent({
  103. type: "tool_result",
  104. tool_use_id: toolCallId,
  105. content: errorMessage,
  106. is_error: true,
  107. })
  108. }
  109. break
  110. }
  111. if (cline.didAlreadyUseTool) {
  112. const toolCallId = mcpBlock.id
  113. 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.`
  114. if (toolCallId) {
  115. cline.pushToolResultToUserContent({
  116. type: "tool_result",
  117. tool_use_id: toolCallId,
  118. content: errorMessage,
  119. is_error: true,
  120. })
  121. }
  122. break
  123. }
  124. // Track if we've already pushed a tool result
  125. let hasToolResult = false
  126. const toolCallId = mcpBlock.id
  127. const toolProtocol = TOOL_PROTOCOL.NATIVE // MCP tools in native mode always use native protocol
  128. // Store approval feedback to merge into tool result (GitHub #10465)
  129. let approvalFeedback: { text: string; images?: string[] } | undefined
  130. const pushToolResult = (content: ToolResponse, feedbackImages?: string[]) => {
  131. if (hasToolResult) {
  132. console.warn(
  133. `[presentAssistantMessage] Skipping duplicate tool_result for mcp_tool_use: ${toolCallId}`,
  134. )
  135. return
  136. }
  137. let resultContent: string
  138. let imageBlocks: Anthropic.ImageBlockParam[] = []
  139. if (typeof content === "string") {
  140. resultContent = content || "(tool did not return anything)"
  141. } else {
  142. const textBlocks = content.filter((item) => item.type === "text")
  143. imageBlocks = content.filter((item) => item.type === "image") as Anthropic.ImageBlockParam[]
  144. resultContent =
  145. textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") ||
  146. "(tool did not return anything)"
  147. }
  148. // Merge approval feedback into tool result (GitHub #10465)
  149. if (approvalFeedback) {
  150. const feedbackText = formatResponse.toolApprovedWithFeedback(approvalFeedback.text, toolProtocol)
  151. resultContent = `${feedbackText}\n\n${resultContent}`
  152. // Add feedback images to the image blocks
  153. if (approvalFeedback.images) {
  154. const feedbackImageBlocks = formatResponse.imageBlocks(approvalFeedback.images)
  155. imageBlocks = [...feedbackImageBlocks, ...imageBlocks]
  156. }
  157. }
  158. if (toolCallId) {
  159. cline.pushToolResultToUserContent({
  160. type: "tool_result",
  161. tool_use_id: toolCallId,
  162. content: resultContent,
  163. })
  164. if (imageBlocks.length > 0) {
  165. cline.userMessageContent.push(...imageBlocks)
  166. }
  167. }
  168. hasToolResult = true
  169. cline.didAlreadyUseTool = true
  170. }
  171. const toolDescription = () => `[mcp_tool: ${mcpBlock.serverName}/${mcpBlock.toolName}]`
  172. const askApproval = async (
  173. type: ClineAsk,
  174. partialMessage?: string,
  175. progressStatus?: ToolProgressStatus,
  176. isProtected?: boolean,
  177. ) => {
  178. const { response, text, images } = await cline.ask(
  179. type,
  180. partialMessage,
  181. false,
  182. progressStatus,
  183. isProtected || false,
  184. )
  185. if (response !== "yesButtonClicked") {
  186. if (text) {
  187. await cline.say("user_feedback", text, images)
  188. pushToolResult(
  189. formatResponse.toolResult(
  190. formatResponse.toolDeniedWithFeedback(text, toolProtocol),
  191. images,
  192. ),
  193. )
  194. } else {
  195. pushToolResult(formatResponse.toolDenied(toolProtocol))
  196. }
  197. cline.didRejectTool = true
  198. return false
  199. }
  200. // Store approval feedback to be merged into tool result (GitHub #10465)
  201. // Don't push it as a separate tool_result here - that would create duplicates.
  202. // The tool will call pushToolResult, which will merge the feedback into the actual result.
  203. if (text) {
  204. await cline.say("user_feedback", text, images)
  205. approvalFeedback = { text, images }
  206. }
  207. return true
  208. }
  209. const handleError = async (action: string, error: Error) => {
  210. // Silently ignore AskIgnoredError - this is an internal control flow
  211. // signal, not an actual error. It occurs when a newer ask supersedes an older one.
  212. if (error instanceof AskIgnoredError) {
  213. return
  214. }
  215. const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}`
  216. await cline.say(
  217. "error",
  218. `Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
  219. )
  220. pushToolResult(formatResponse.toolError(errorString, toolProtocol))
  221. }
  222. if (!mcpBlock.partial) {
  223. cline.recordToolUsage("use_mcp_tool") // Record as use_mcp_tool for analytics
  224. TelemetryService.instance.captureToolUsage(cline.taskId, "use_mcp_tool", toolProtocol)
  225. }
  226. // Resolve sanitized server name back to original server name
  227. // The serverName from parsing is sanitized (e.g., "my_server" from "my server")
  228. // We need the original name to find the actual MCP connection
  229. const mcpHub = cline.providerRef.deref()?.getMcpHub()
  230. let resolvedServerName = mcpBlock.serverName
  231. if (mcpHub) {
  232. const originalName = mcpHub.findServerNameBySanitizedName(mcpBlock.serverName)
  233. if (originalName) {
  234. resolvedServerName = originalName
  235. }
  236. }
  237. // Execute the MCP tool using the same handler as use_mcp_tool
  238. // Create a synthetic ToolUse block that the useMcpToolTool can handle
  239. const syntheticToolUse: ToolUse<"use_mcp_tool"> = {
  240. type: "tool_use",
  241. id: mcpBlock.id,
  242. name: "use_mcp_tool",
  243. params: {
  244. server_name: resolvedServerName,
  245. tool_name: mcpBlock.toolName,
  246. arguments: JSON.stringify(mcpBlock.arguments),
  247. },
  248. partial: mcpBlock.partial,
  249. nativeArgs: {
  250. server_name: resolvedServerName,
  251. tool_name: mcpBlock.toolName,
  252. arguments: mcpBlock.arguments,
  253. },
  254. }
  255. await useMcpToolTool.handle(cline, syntheticToolUse, {
  256. askApproval,
  257. handleError,
  258. pushToolResult,
  259. removeClosingTag: (tag, text) => text || "",
  260. toolProtocol,
  261. })
  262. break
  263. }
  264. case "text": {
  265. if (cline.didRejectTool || cline.didAlreadyUseTool) {
  266. break
  267. }
  268. let content = block.content
  269. if (content) {
  270. // Have to do this for partial and complete since sending
  271. // content in thinking tags to markdown renderer will
  272. // automatically be removed.
  273. // Remove end substrings of <thinking or </thinking (below xml
  274. // parsing is only for opening tags).
  275. // Tthis is done with the xml parsing below now, but keeping
  276. // here for reference.
  277. // content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?$/, "")
  278. //
  279. // Remove all instances of <thinking> (with optional line break
  280. // after) and </thinking> (with optional line break before).
  281. // - Needs to be separate since we dont want to remove the line
  282. // break before the first tag.
  283. // - Needs to happen before the xml parsing below.
  284. content = content.replace(/<thinking>\s?/g, "")
  285. content = content.replace(/\s?<\/thinking>/g, "")
  286. // Remove partial XML tag at the very end of the content (for
  287. // tool use and thinking tags), Prevents scrollview from
  288. // jumping when tags are automatically removed.
  289. const lastOpenBracketIndex = content.lastIndexOf("<")
  290. if (lastOpenBracketIndex !== -1) {
  291. const possibleTag = content.slice(lastOpenBracketIndex)
  292. // Check if there's a '>' after the last '<' (i.e., if the
  293. // tag is complete) (complete thinking and tool tags will
  294. // have been removed by now.)
  295. const hasCloseBracket = possibleTag.includes(">")
  296. if (!hasCloseBracket) {
  297. // Extract the potential tag name.
  298. let tagContent: string
  299. if (possibleTag.startsWith("</")) {
  300. tagContent = possibleTag.slice(2).trim()
  301. } else {
  302. tagContent = possibleTag.slice(1).trim()
  303. }
  304. // Check if tagContent is likely an incomplete tag name
  305. // (letters and underscores only).
  306. const isLikelyTagName = /^[a-zA-Z_]+$/.test(tagContent)
  307. // Preemptively remove < or </ to keep from these
  308. // artifacts showing up in chat (also handles closing
  309. // thinking tags).
  310. const isOpeningOrClosing = possibleTag === "<" || possibleTag === "</"
  311. // If the tag is incomplete and at the end, remove it
  312. // from the content.
  313. if (isOpeningOrClosing || isLikelyTagName) {
  314. content = content.slice(0, lastOpenBracketIndex).trim()
  315. }
  316. }
  317. }
  318. }
  319. await cline.say("text", content, undefined, block.partial)
  320. break
  321. }
  322. case "tool_use": {
  323. // Fetch state early so it's available for toolDescription and validation
  324. const state = await cline.providerRef.deref()?.getState()
  325. const { mode, customModes, experiments: stateExperiments } = state ?? {}
  326. const toolDescription = (): string => {
  327. switch (block.name) {
  328. case "execute_command":
  329. return `[${block.name} for '${block.params.command}']`
  330. case "read_file":
  331. // Prefer native typed args when available; fall back to legacy params
  332. // Check if nativeArgs exists (native protocol)
  333. if (block.nativeArgs) {
  334. return readFileTool.getReadFileToolDescription(block.name, block.nativeArgs)
  335. }
  336. return readFileTool.getReadFileToolDescription(block.name, block.params)
  337. case "fetch_instructions":
  338. return `[${block.name} for '${block.params.task}']`
  339. case "write_to_file":
  340. return `[${block.name} for '${block.params.path}']`
  341. case "apply_diff":
  342. // Handle both legacy format and new multi-file format
  343. if (block.params.path) {
  344. return `[${block.name} for '${block.params.path}']`
  345. } else if (block.params.args) {
  346. // Try to extract first file path from args for display
  347. const match = block.params.args.match(/<file>.*?<path>([^<]+)<\/path>/s)
  348. if (match) {
  349. const firstPath = match[1]
  350. // Check if there are multiple files
  351. const fileCount = (block.params.args.match(/<file>/g) || []).length
  352. if (fileCount > 1) {
  353. return `[${block.name} for '${firstPath}' and ${fileCount - 1} more file${fileCount > 2 ? "s" : ""}]`
  354. } else {
  355. return `[${block.name} for '${firstPath}']`
  356. }
  357. }
  358. }
  359. return `[${block.name}]`
  360. case "search_files":
  361. return `[${block.name} for '${block.params.regex}'${
  362. block.params.file_pattern ? ` in '${block.params.file_pattern}'` : ""
  363. }]`
  364. case "search_and_replace":
  365. return `[${block.name} for '${block.params.path}']`
  366. case "search_replace":
  367. return `[${block.name} for '${block.params.file_path}']`
  368. case "edit_file":
  369. return `[${block.name} for '${block.params.file_path}']`
  370. case "apply_patch":
  371. return `[${block.name}]`
  372. case "list_files":
  373. return `[${block.name} for '${block.params.path}']`
  374. case "browser_action":
  375. return `[${block.name} for '${block.params.action}']`
  376. case "use_mcp_tool":
  377. return `[${block.name} for '${block.params.server_name}']`
  378. case "access_mcp_resource":
  379. return `[${block.name} for '${block.params.server_name}']`
  380. case "ask_followup_question":
  381. return `[${block.name} for '${block.params.question}']`
  382. case "attempt_completion":
  383. return `[${block.name}]`
  384. case "switch_mode":
  385. return `[${block.name} to '${block.params.mode_slug}'${block.params.reason ? ` because: ${block.params.reason}` : ""}]`
  386. case "codebase_search": // Add case for the new tool
  387. return `[${block.name} for '${block.params.query}']`
  388. case "update_todo_list":
  389. return `[${block.name}]`
  390. case "new_task": {
  391. const mode = block.params.mode ?? defaultModeSlug
  392. const message = block.params.message ?? "(no message)"
  393. const modeName = getModeBySlug(mode, customModes)?.name ?? mode
  394. return `[${block.name} in ${modeName} mode: '${message}']`
  395. }
  396. case "run_slash_command":
  397. return `[${block.name} for '${block.params.command}'${block.params.args ? ` with args: ${block.params.args}` : ""}]`
  398. case "generate_image":
  399. return `[${block.name} for '${block.params.path}']`
  400. default:
  401. return `[${block.name}]`
  402. }
  403. }
  404. if (cline.didRejectTool) {
  405. // Ignore any tool content after user has rejected tool once.
  406. // For native protocol, we must send a tool_result for every tool_use to avoid API errors
  407. const toolCallId = block.id
  408. const errorMessage = !block.partial
  409. ? `Skipping tool ${toolDescription()} due to user rejecting a previous tool.`
  410. : `Tool ${toolDescription()} was interrupted and not executed due to user rejecting a previous tool.`
  411. if (toolCallId) {
  412. // Native protocol: MUST send tool_result for every tool_use
  413. cline.pushToolResultToUserContent({
  414. type: "tool_result",
  415. tool_use_id: toolCallId,
  416. content: errorMessage,
  417. is_error: true,
  418. })
  419. } else {
  420. // XML protocol: send as text
  421. cline.userMessageContent.push({
  422. type: "text",
  423. text: errorMessage,
  424. })
  425. }
  426. break
  427. }
  428. if (cline.didAlreadyUseTool) {
  429. // Ignore any content after a tool has already been used.
  430. // For native protocol, we must send a tool_result for every tool_use to avoid API errors
  431. const toolCallId = block.id
  432. 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.`
  433. if (toolCallId) {
  434. // Native protocol: MUST send tool_result for every tool_use
  435. cline.pushToolResultToUserContent({
  436. type: "tool_result",
  437. tool_use_id: toolCallId,
  438. content: errorMessage,
  439. is_error: true,
  440. })
  441. } else {
  442. // XML protocol: send as text
  443. cline.userMessageContent.push({
  444. type: "text",
  445. text: errorMessage,
  446. })
  447. }
  448. break
  449. }
  450. // Track if we've already pushed a tool result for this tool call (native protocol only)
  451. let hasToolResult = false
  452. // Determine protocol by checking if this tool call has an ID.
  453. // Native protocol tool calls ALWAYS have an ID (set when parsed from tool_call chunks).
  454. // XML protocol tool calls NEVER have an ID (parsed from XML text).
  455. const toolCallId = (block as any).id
  456. const toolProtocol = toolCallId ? TOOL_PROTOCOL.NATIVE : TOOL_PROTOCOL.XML
  457. // Multiple native tool calls feature is on hold - always disabled
  458. // Previously resolved from experiments.isEnabled(..., EXPERIMENT_IDS.MULTIPLE_NATIVE_TOOL_CALLS)
  459. const isMultipleNativeToolCallsEnabled = false
  460. // Store approval feedback to merge into tool result (GitHub #10465)
  461. let approvalFeedback: { text: string; images?: string[] } | undefined
  462. const pushToolResult = (content: ToolResponse) => {
  463. if (toolProtocol === TOOL_PROTOCOL.NATIVE) {
  464. // For native protocol, only allow ONE tool_result per tool call
  465. if (hasToolResult) {
  466. console.warn(
  467. `[presentAssistantMessage] Skipping duplicate tool_result for tool_use_id: ${toolCallId}`,
  468. )
  469. return
  470. }
  471. // For native protocol, tool_result content must be a string
  472. // Images are added as separate blocks in the user message
  473. let resultContent: string
  474. let imageBlocks: Anthropic.ImageBlockParam[] = []
  475. if (typeof content === "string") {
  476. resultContent = content || "(tool did not return anything)"
  477. } else {
  478. // Separate text and image blocks
  479. const textBlocks = content.filter((item) => item.type === "text")
  480. imageBlocks = content.filter((item) => item.type === "image") as Anthropic.ImageBlockParam[]
  481. // Convert text blocks to string for tool_result
  482. resultContent =
  483. textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") ||
  484. "(tool did not return anything)"
  485. }
  486. // Merge approval feedback into tool result (GitHub #10465)
  487. if (approvalFeedback) {
  488. const feedbackText = formatResponse.toolApprovedWithFeedback(
  489. approvalFeedback.text,
  490. toolProtocol,
  491. )
  492. resultContent = `${feedbackText}\n\n${resultContent}`
  493. // Add feedback images to the image blocks
  494. if (approvalFeedback.images) {
  495. const feedbackImageBlocks = formatResponse.imageBlocks(approvalFeedback.images)
  496. imageBlocks = [...feedbackImageBlocks, ...imageBlocks]
  497. }
  498. }
  499. // Add tool_result with text content only
  500. cline.pushToolResultToUserContent({
  501. type: "tool_result",
  502. tool_use_id: toolCallId,
  503. content: resultContent,
  504. })
  505. // Add image blocks separately after tool_result
  506. if (imageBlocks.length > 0) {
  507. cline.userMessageContent.push(...imageBlocks)
  508. }
  509. hasToolResult = true
  510. } else {
  511. // For XML protocol, add as text blocks (legacy behavior)
  512. let resultContent: string
  513. if (typeof content === "string") {
  514. resultContent = content || "(tool did not return anything)"
  515. } else {
  516. const textBlocks = content.filter((item) => item.type === "text")
  517. resultContent =
  518. textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") ||
  519. "(tool did not return anything)"
  520. }
  521. // Merge approval feedback into tool result (GitHub #10465)
  522. if (approvalFeedback) {
  523. const feedbackText = formatResponse.toolApprovedWithFeedback(
  524. approvalFeedback.text,
  525. toolProtocol,
  526. )
  527. resultContent = `${feedbackText}\n\n${resultContent}`
  528. }
  529. cline.userMessageContent.push({ type: "text", text: `${toolDescription()} Result:` })
  530. if (typeof content === "string") {
  531. cline.userMessageContent.push({
  532. type: "text",
  533. text: resultContent,
  534. })
  535. } else {
  536. // Add text content with merged feedback
  537. cline.userMessageContent.push({
  538. type: "text",
  539. text: resultContent,
  540. })
  541. // Add any images from the tool result
  542. const imageBlocks = content.filter((item) => item.type === "image")
  543. if (imageBlocks.length > 0) {
  544. cline.userMessageContent.push(...imageBlocks)
  545. }
  546. }
  547. }
  548. // For XML protocol: Only one tool per message is allowed
  549. // For native protocol with experimental flag enabled: Multiple tools can be executed in sequence
  550. // For native protocol with experimental flag disabled: Single tool per message (default safe behavior)
  551. if (toolProtocol === TOOL_PROTOCOL.XML) {
  552. // Once a tool result has been collected, ignore all other tool
  553. // uses since we should only ever present one tool result per
  554. // message (XML protocol only).
  555. cline.didAlreadyUseTool = true
  556. } else if (toolProtocol === TOOL_PROTOCOL.NATIVE && !isMultipleNativeToolCallsEnabled) {
  557. // For native protocol with experimental flag disabled, enforce single tool per message
  558. cline.didAlreadyUseTool = true
  559. }
  560. // If toolProtocol is NATIVE and isMultipleNativeToolCallsEnabled is true,
  561. // allow multiple tool calls in sequence (don't set didAlreadyUseTool)
  562. }
  563. const askApproval = async (
  564. type: ClineAsk,
  565. partialMessage?: string,
  566. progressStatus?: ToolProgressStatus,
  567. isProtected?: boolean,
  568. ) => {
  569. const { response, text, images } = await cline.ask(
  570. type,
  571. partialMessage,
  572. false,
  573. progressStatus,
  574. isProtected || false,
  575. )
  576. if (response !== "yesButtonClicked") {
  577. // Handle both messageResponse and noButtonClicked with text.
  578. if (text) {
  579. await cline.say("user_feedback", text, images)
  580. pushToolResult(
  581. formatResponse.toolResult(
  582. formatResponse.toolDeniedWithFeedback(text, toolProtocol),
  583. images,
  584. ),
  585. )
  586. } else {
  587. pushToolResult(formatResponse.toolDenied(toolProtocol))
  588. }
  589. cline.didRejectTool = true
  590. return false
  591. }
  592. // Store approval feedback to be merged into tool result (GitHub #10465)
  593. // Don't push it as a separate tool_result here - that would create duplicates.
  594. // The tool will call pushToolResult, which will merge the feedback into the actual result.
  595. if (text) {
  596. await cline.say("user_feedback", text, images)
  597. approvalFeedback = { text, images }
  598. }
  599. return true
  600. }
  601. const askFinishSubTaskApproval = async () => {
  602. // Ask the user to approve this task has completed, and he has
  603. // reviewed it, and we can declare task is finished and return
  604. // control to the parent task to continue running the rest of
  605. // the sub-tasks.
  606. const toolMessage = JSON.stringify({ tool: "finishTask" })
  607. return await askApproval("tool", toolMessage)
  608. }
  609. const handleError = async (action: string, error: Error) => {
  610. // Silently ignore AskIgnoredError - this is an internal control flow
  611. // signal, not an actual error. It occurs when a newer ask supersedes an older one.
  612. if (error instanceof AskIgnoredError) {
  613. return
  614. }
  615. const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}`
  616. await cline.say(
  617. "error",
  618. `Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
  619. )
  620. pushToolResult(formatResponse.toolError(errorString, toolProtocol))
  621. }
  622. // If block is partial, remove partial closing tag so its not
  623. // presented to user.
  624. const removeClosingTag = (tag: ToolParamName, text?: string): string => {
  625. if (!block.partial) {
  626. return text || ""
  627. }
  628. if (!text) {
  629. return ""
  630. }
  631. // This regex dynamically constructs a pattern to match the
  632. // closing tag:
  633. // - Optionally matches whitespace before the tag.
  634. // - Matches '<' or '</' optionally followed by any subset of
  635. // characters from the tag name.
  636. const tagRegex = new RegExp(
  637. `\\s?<\/?${tag
  638. .split("")
  639. .map((char) => `(?:${char})?`)
  640. .join("")}$`,
  641. "g",
  642. )
  643. return text.replace(tagRegex, "")
  644. }
  645. // Keep browser open during an active session so other tools can run.
  646. // Session is active if we've seen any browser_action_result and the last browser_action is not "close".
  647. try {
  648. const messages = cline.clineMessages || []
  649. const hasStarted = messages.some((m: any) => m.say === "browser_action_result")
  650. let isClosed = false
  651. for (let i = messages.length - 1; i >= 0; i--) {
  652. const m = messages[i]
  653. if (m.say === "browser_action") {
  654. try {
  655. const act = JSON.parse(m.text || "{}")
  656. isClosed = act.action === "close"
  657. } catch {}
  658. break
  659. }
  660. }
  661. const sessionActive = hasStarted && !isClosed
  662. // Only auto-close when no active browser session is present, and this isn't a browser_action
  663. if (!sessionActive && block.name !== "browser_action") {
  664. await cline.browserSession.closeBrowser()
  665. }
  666. } catch {
  667. // On any unexpected error, fall back to conservative behavior
  668. if (block.name !== "browser_action") {
  669. await cline.browserSession.closeBrowser()
  670. }
  671. }
  672. if (!block.partial) {
  673. // Check if this is a custom tool - if so, record as "custom_tool" (like MCP tools)
  674. const isCustomTool = stateExperiments?.customTools && customToolRegistry.has(block.name)
  675. const recordName = isCustomTool ? "custom_tool" : block.name
  676. cline.recordToolUsage(recordName)
  677. TelemetryService.instance.captureToolUsage(cline.taskId, recordName, toolProtocol)
  678. }
  679. // Validate tool use before execution - ONLY for complete (non-partial) blocks.
  680. // Validating partial blocks would cause validation errors to be thrown repeatedly
  681. // during streaming, pushing multiple tool_results for the same tool_use_id and
  682. // potentially causing the stream to appear frozen.
  683. if (!block.partial) {
  684. const modelInfo = cline.api.getModel()
  685. // Resolve aliases in includedTools before validation
  686. // e.g., "edit_file" should resolve to "apply_diff"
  687. const rawIncludedTools = modelInfo?.info?.includedTools
  688. const { resolveToolAlias } = await import("../prompts/tools/filter-tools-for-mode")
  689. const includedTools = rawIncludedTools?.map((tool) => resolveToolAlias(tool))
  690. try {
  691. validateToolUse(
  692. block.name as ToolName,
  693. mode ?? defaultModeSlug,
  694. customModes ?? [],
  695. { apply_diff: cline.diffEnabled },
  696. block.params,
  697. stateExperiments,
  698. includedTools,
  699. )
  700. } catch (error) {
  701. cline.consecutiveMistakeCount++
  702. // For validation errors (unknown tool, tool not allowed for mode), we need to:
  703. // 1. Send a tool_result with the error (required for native protocol)
  704. // 2. NOT set didAlreadyUseTool = true (the tool was never executed, just failed validation)
  705. // This prevents the stream from being interrupted with "Response interrupted by tool use result"
  706. // which would cause the extension to appear to hang
  707. const errorContent = formatResponse.toolError(error.message, toolProtocol)
  708. if (toolProtocol === TOOL_PROTOCOL.NATIVE && toolCallId) {
  709. // For native protocol, push tool_result directly without setting didAlreadyUseTool
  710. cline.pushToolResultToUserContent({
  711. type: "tool_result",
  712. tool_use_id: toolCallId,
  713. content: typeof errorContent === "string" ? errorContent : "(validation error)",
  714. is_error: true,
  715. })
  716. } else {
  717. // For XML protocol, use the standard pushToolResult
  718. pushToolResult(errorContent)
  719. }
  720. break
  721. }
  722. }
  723. // Check for identical consecutive tool calls.
  724. if (!block.partial) {
  725. // Use the detector to check for repetition, passing the ToolUse
  726. // block directly.
  727. const repetitionCheck = cline.toolRepetitionDetector.check(block)
  728. // If execution is not allowed, notify user and break.
  729. if (!repetitionCheck.allowExecution && repetitionCheck.askUser) {
  730. // Handle repetition similar to mistake_limit_reached pattern.
  731. const { response, text, images } = await cline.ask(
  732. repetitionCheck.askUser.messageKey as ClineAsk,
  733. repetitionCheck.askUser.messageDetail.replace("{toolName}", block.name),
  734. )
  735. if (response === "messageResponse") {
  736. // Add user feedback to userContent.
  737. cline.userMessageContent.push(
  738. {
  739. type: "text" as const,
  740. text: `Tool repetition limit reached. User feedback: ${text}`,
  741. },
  742. ...formatResponse.imageBlocks(images),
  743. )
  744. // Add user feedback to chat.
  745. await cline.say("user_feedback", text, images)
  746. }
  747. // Track tool repetition in telemetry via PostHog exception tracking and event.
  748. TelemetryService.instance.captureConsecutiveMistakeError(cline.taskId)
  749. TelemetryService.instance.captureException(
  750. new ConsecutiveMistakeError(
  751. `Tool repetition limit reached for ${block.name}`,
  752. cline.taskId,
  753. cline.consecutiveMistakeCount,
  754. cline.consecutiveMistakeLimit,
  755. "tool_repetition",
  756. cline.apiConfiguration.apiProvider,
  757. cline.api.getModel().id,
  758. ),
  759. )
  760. // Return tool result message about the repetition
  761. pushToolResult(
  762. formatResponse.toolError(
  763. `Tool call repetition limit reached for ${block.name}. Please try a different approach.`,
  764. toolProtocol,
  765. ),
  766. )
  767. break
  768. }
  769. }
  770. switch (block.name) {
  771. case "write_to_file":
  772. await checkpointSaveAndMark(cline)
  773. await writeToFileTool.handle(cline, block as ToolUse<"write_to_file">, {
  774. askApproval,
  775. handleError,
  776. pushToolResult,
  777. removeClosingTag,
  778. toolProtocol,
  779. })
  780. break
  781. case "update_todo_list":
  782. await updateTodoListTool.handle(cline, block as ToolUse<"update_todo_list">, {
  783. askApproval,
  784. handleError,
  785. pushToolResult,
  786. removeClosingTag,
  787. toolProtocol,
  788. })
  789. break
  790. case "apply_diff": {
  791. await checkpointSaveAndMark(cline)
  792. // Check if this tool call came from native protocol by checking for ID
  793. // Native calls always have IDs, XML calls never do
  794. if (toolProtocol === TOOL_PROTOCOL.NATIVE) {
  795. await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
  796. askApproval,
  797. handleError,
  798. pushToolResult,
  799. removeClosingTag,
  800. toolProtocol,
  801. })
  802. break
  803. }
  804. // Get the provider and state to check experiment settings
  805. const provider = cline.providerRef.deref()
  806. let isMultiFileApplyDiffEnabled = false
  807. if (provider) {
  808. const state = await provider.getState()
  809. isMultiFileApplyDiffEnabled = experiments.isEnabled(
  810. state.experiments ?? {},
  811. EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF,
  812. )
  813. }
  814. if (isMultiFileApplyDiffEnabled) {
  815. await applyDiffTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
  816. } else {
  817. await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
  818. askApproval,
  819. handleError,
  820. pushToolResult,
  821. removeClosingTag,
  822. toolProtocol,
  823. })
  824. }
  825. break
  826. }
  827. case "search_and_replace":
  828. await checkpointSaveAndMark(cline)
  829. await searchAndReplaceTool.handle(cline, block as ToolUse<"search_and_replace">, {
  830. askApproval,
  831. handleError,
  832. pushToolResult,
  833. removeClosingTag,
  834. toolProtocol,
  835. })
  836. break
  837. case "search_replace":
  838. await checkpointSaveAndMark(cline)
  839. await searchReplaceTool.handle(cline, block as ToolUse<"search_replace">, {
  840. askApproval,
  841. handleError,
  842. pushToolResult,
  843. removeClosingTag,
  844. toolProtocol,
  845. })
  846. break
  847. case "edit_file":
  848. await checkpointSaveAndMark(cline)
  849. await editFileTool.handle(cline, block as ToolUse<"edit_file">, {
  850. askApproval,
  851. handleError,
  852. pushToolResult,
  853. removeClosingTag,
  854. toolProtocol,
  855. })
  856. break
  857. case "apply_patch":
  858. await checkpointSaveAndMark(cline)
  859. await applyPatchTool.handle(cline, block as ToolUse<"apply_patch">, {
  860. askApproval,
  861. handleError,
  862. pushToolResult,
  863. removeClosingTag,
  864. toolProtocol,
  865. })
  866. break
  867. case "read_file":
  868. // Type assertion is safe here because we're in the "read_file" case
  869. await readFileTool.handle(cline, block as ToolUse<"read_file">, {
  870. askApproval,
  871. handleError,
  872. pushToolResult,
  873. removeClosingTag,
  874. toolProtocol,
  875. })
  876. break
  877. case "fetch_instructions":
  878. await fetchInstructionsTool.handle(cline, block as ToolUse<"fetch_instructions">, {
  879. askApproval,
  880. handleError,
  881. pushToolResult,
  882. removeClosingTag,
  883. toolProtocol,
  884. })
  885. break
  886. case "list_files":
  887. await listFilesTool.handle(cline, block as ToolUse<"list_files">, {
  888. askApproval,
  889. handleError,
  890. pushToolResult,
  891. removeClosingTag,
  892. toolProtocol,
  893. })
  894. break
  895. case "codebase_search":
  896. await codebaseSearchTool.handle(cline, block as ToolUse<"codebase_search">, {
  897. askApproval,
  898. handleError,
  899. pushToolResult,
  900. removeClosingTag,
  901. toolProtocol,
  902. })
  903. break
  904. case "search_files":
  905. await searchFilesTool.handle(cline, block as ToolUse<"search_files">, {
  906. askApproval,
  907. handleError,
  908. pushToolResult,
  909. removeClosingTag,
  910. toolProtocol,
  911. })
  912. break
  913. case "browser_action":
  914. await browserActionTool(
  915. cline,
  916. block as ToolUse<"browser_action">,
  917. askApproval,
  918. handleError,
  919. pushToolResult,
  920. removeClosingTag,
  921. )
  922. break
  923. case "execute_command":
  924. await executeCommandTool.handle(cline, block as ToolUse<"execute_command">, {
  925. askApproval,
  926. handleError,
  927. pushToolResult,
  928. removeClosingTag,
  929. toolProtocol,
  930. })
  931. break
  932. case "use_mcp_tool":
  933. await useMcpToolTool.handle(cline, block as ToolUse<"use_mcp_tool">, {
  934. askApproval,
  935. handleError,
  936. pushToolResult,
  937. removeClosingTag,
  938. toolProtocol,
  939. })
  940. break
  941. case "access_mcp_resource":
  942. await accessMcpResourceTool.handle(cline, block as ToolUse<"access_mcp_resource">, {
  943. askApproval,
  944. handleError,
  945. pushToolResult,
  946. removeClosingTag,
  947. toolProtocol,
  948. })
  949. break
  950. case "ask_followup_question":
  951. await askFollowupQuestionTool.handle(cline, block as ToolUse<"ask_followup_question">, {
  952. askApproval,
  953. handleError,
  954. pushToolResult,
  955. removeClosingTag,
  956. toolProtocol,
  957. })
  958. break
  959. case "switch_mode":
  960. await switchModeTool.handle(cline, block as ToolUse<"switch_mode">, {
  961. askApproval,
  962. handleError,
  963. pushToolResult,
  964. removeClosingTag,
  965. toolProtocol,
  966. })
  967. break
  968. case "new_task":
  969. await newTaskTool.handle(cline, block as ToolUse<"new_task">, {
  970. askApproval,
  971. handleError,
  972. pushToolResult,
  973. removeClosingTag,
  974. toolProtocol,
  975. toolCallId: block.id,
  976. })
  977. break
  978. case "attempt_completion": {
  979. const completionCallbacks: AttemptCompletionCallbacks = {
  980. askApproval,
  981. handleError,
  982. pushToolResult,
  983. removeClosingTag,
  984. askFinishSubTaskApproval,
  985. toolDescription,
  986. toolProtocol,
  987. }
  988. await attemptCompletionTool.handle(
  989. cline,
  990. block as ToolUse<"attempt_completion">,
  991. completionCallbacks,
  992. )
  993. break
  994. }
  995. case "run_slash_command":
  996. await runSlashCommandTool.handle(cline, block as ToolUse<"run_slash_command">, {
  997. askApproval,
  998. handleError,
  999. pushToolResult,
  1000. removeClosingTag,
  1001. toolProtocol,
  1002. })
  1003. break
  1004. case "generate_image":
  1005. await checkpointSaveAndMark(cline)
  1006. await generateImageTool.handle(cline, block as ToolUse<"generate_image">, {
  1007. askApproval,
  1008. handleError,
  1009. pushToolResult,
  1010. removeClosingTag,
  1011. toolProtocol,
  1012. })
  1013. break
  1014. default: {
  1015. // Handle unknown/invalid tool names OR custom tools
  1016. // This is critical for native protocol where every tool_use MUST have a tool_result
  1017. // CRITICAL: Don't process partial blocks for unknown tools - just let them stream in.
  1018. // If we try to show errors for partial blocks, we'd show the error on every streaming chunk,
  1019. // creating a loop that appears to freeze the extension. Only handle complete blocks.
  1020. if (block.partial) {
  1021. break
  1022. }
  1023. const customTool = stateExperiments?.customTools ? customToolRegistry.get(block.name) : undefined
  1024. if (customTool) {
  1025. try {
  1026. let customToolArgs
  1027. if (customTool.parameters) {
  1028. try {
  1029. customToolArgs = customTool.parameters.parse(block.nativeArgs || block.params || {})
  1030. } catch (parseParamsError) {
  1031. const message = `Custom tool "${block.name}" argument validation failed: ${parseParamsError.message}`
  1032. console.error(message)
  1033. cline.consecutiveMistakeCount++
  1034. await cline.say("error", message)
  1035. pushToolResult(formatResponse.toolError(message, toolProtocol))
  1036. break
  1037. }
  1038. }
  1039. const result = await customTool.execute(customToolArgs, {
  1040. mode: mode ?? defaultModeSlug,
  1041. task: cline,
  1042. })
  1043. console.log(
  1044. `${customTool.name}.execute(): ${JSON.stringify(customToolArgs)} -> ${JSON.stringify(result)}`,
  1045. )
  1046. pushToolResult(result)
  1047. cline.consecutiveMistakeCount = 0
  1048. } catch (executionError: any) {
  1049. cline.consecutiveMistakeCount++
  1050. // Record custom tool error with static name
  1051. cline.recordToolError("custom_tool", executionError.message)
  1052. await handleError(`executing custom tool "${block.name}"`, executionError)
  1053. }
  1054. break
  1055. }
  1056. // Not a custom tool - handle as unknown tool error
  1057. const errorMessage = `Unknown tool "${block.name}". This tool does not exist. Please use one of the available tools.`
  1058. cline.consecutiveMistakeCount++
  1059. cline.recordToolError(block.name as ToolName, errorMessage)
  1060. await cline.say("error", t("tools:unknownToolError", { toolName: block.name }))
  1061. // Push tool_result directly for native protocol WITHOUT setting didAlreadyUseTool
  1062. // This prevents the stream from being interrupted with "Response interrupted by tool use result"
  1063. if (toolProtocol === TOOL_PROTOCOL.NATIVE && toolCallId) {
  1064. cline.pushToolResultToUserContent({
  1065. type: "tool_result",
  1066. tool_use_id: toolCallId,
  1067. content: formatResponse.toolError(errorMessage, toolProtocol),
  1068. is_error: true,
  1069. })
  1070. } else {
  1071. pushToolResult(formatResponse.toolError(errorMessage, toolProtocol))
  1072. }
  1073. break
  1074. }
  1075. }
  1076. break
  1077. }
  1078. }
  1079. // Seeing out of bounds is fine, it means that the next too call is being
  1080. // built up and ready to add to assistantMessageContent to present.
  1081. // When you see the UI inactive during this, it means that a tool is
  1082. // breaking without presenting any UI. For example the write_to_file tool
  1083. // was breaking when relpath was undefined, and for invalid relpath it never
  1084. // presented UI.
  1085. // This needs to be placed here, if not then calling
  1086. // cline.presentAssistantMessage below would fail (sometimes) since it's
  1087. // locked.
  1088. cline.presentAssistantMessageLocked = false
  1089. // NOTE: When tool is rejected, iterator stream is interrupted and it waits
  1090. // for `userMessageContentReady` to be true. Future calls to present will
  1091. // skip execution since `didRejectTool` and iterate until `contentIndex` is
  1092. // set to message length and it sets userMessageContentReady to true itself
  1093. // (instead of preemptively doing it in iterator).
  1094. if (!block.partial || cline.didRejectTool || cline.didAlreadyUseTool) {
  1095. // Block is finished streaming and executing.
  1096. if (cline.currentStreamingContentIndex === cline.assistantMessageContent.length - 1) {
  1097. // It's okay that we increment if !didCompleteReadingStream, it'll
  1098. // just return because out of bounds and as streaming continues it
  1099. // will call `presentAssitantMessage` if a new block is ready. If
  1100. // streaming is finished then we set `userMessageContentReady` to
  1101. // true when out of bounds. This gracefully allows the stream to
  1102. // continue on and all potential content blocks be presented.
  1103. // Last block is complete and it is finished executing
  1104. cline.userMessageContentReady = true // Will allow `pWaitFor` to continue.
  1105. }
  1106. // Call next block if it exists (if not then read stream will call it
  1107. // when it's ready).
  1108. // Need to increment regardless, so when read stream calls this function
  1109. // again it will be streaming the next block.
  1110. cline.currentStreamingContentIndex++
  1111. if (cline.currentStreamingContentIndex < cline.assistantMessageContent.length) {
  1112. // There are already more content blocks to stream, so we'll call
  1113. // this function ourselves.
  1114. presentAssistantMessage(cline)
  1115. return
  1116. } else {
  1117. // CRITICAL FIX: If we're out of bounds and the stream is complete, set userMessageContentReady
  1118. // This handles the case where assistantMessageContent is empty or becomes empty after processing
  1119. if (cline.didCompleteReadingStream) {
  1120. cline.userMessageContentReady = true
  1121. }
  1122. }
  1123. }
  1124. // Block is partial, but the read stream may have finished.
  1125. if (cline.presentAssistantMessageHasPendingUpdates) {
  1126. presentAssistantMessage(cline)
  1127. }
  1128. }
  1129. /**
  1130. * save checkpoint and mark done in the current streaming task.
  1131. * @param task The Task instance to checkpoint save and mark.
  1132. * @returns
  1133. */
  1134. async function checkpointSaveAndMark(task: Task) {
  1135. if (task.currentStreamingDidCheckpoint) {
  1136. return
  1137. }
  1138. try {
  1139. await task.checkpointSave(true)
  1140. task.currentStreamingDidCheckpoint = true
  1141. } catch (error) {
  1142. console.error(`[Task#presentAssistantMessage] Error saving checkpoint: ${error.message}`, error)
  1143. }
  1144. }