presentAssistantMessage.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  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 { TelemetryService } from "@roo-code/telemetry"
  6. import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
  7. import type { ToolParamName, ToolResponse, ToolUse } from "../../shared/tools"
  8. import { Package } from "../../shared/package"
  9. import { fetchInstructionsTool } from "../tools/FetchInstructionsTool"
  10. import { listFilesTool } from "../tools/ListFilesTool"
  11. import { readFileTool } from "../tools/ReadFileTool"
  12. import { getSimpleReadFileToolDescription, simpleReadFileTool } from "../tools/simpleReadFileTool"
  13. import { shouldUseSingleFileRead } from "@roo-code/types"
  14. import { writeToFileTool } from "../tools/WriteToFileTool"
  15. import { applyDiffTool } from "../tools/MultiApplyDiffTool"
  16. import { insertContentTool } from "../tools/InsertContentTool"
  17. import { listCodeDefinitionNamesTool } from "../tools/ListCodeDefinitionNamesTool"
  18. import { searchFilesTool } from "../tools/SearchFilesTool"
  19. import { browserActionTool } from "../tools/BrowserActionTool"
  20. import { executeCommandTool } from "../tools/ExecuteCommandTool"
  21. import { useMcpToolTool } from "../tools/UseMcpToolTool"
  22. import { accessMcpResourceTool } from "../tools/accessMcpResourceTool"
  23. import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool"
  24. import { switchModeTool } from "../tools/SwitchModeTool"
  25. import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/AttemptCompletionTool"
  26. import { newTaskTool } from "../tools/NewTaskTool"
  27. import { updateTodoListTool } from "../tools/UpdateTodoListTool"
  28. import { runSlashCommandTool } from "../tools/RunSlashCommandTool"
  29. import { generateImageTool } from "../tools/GenerateImageTool"
  30. import { formatResponse } from "../prompts/responses"
  31. import { validateToolUse } from "../tools/validateToolUse"
  32. import { Task } from "../task/Task"
  33. import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
  34. import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
  35. import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
  36. import * as vscode from "vscode"
  37. import { ToolProtocol, isNativeProtocol } from "@roo-code/types"
  38. /**
  39. * Processes and presents assistant message content to the user interface.
  40. *
  41. * This function is the core message handling system that:
  42. * - Sequentially processes content blocks from the assistant's response.
  43. * - Displays text content to the user.
  44. * - Executes tool use requests with appropriate user approval.
  45. * - Manages the flow of conversation by determining when to proceed to the next content block.
  46. * - Coordinates file system checkpointing for modified files.
  47. * - Controls the conversation state to determine when to continue to the next request.
  48. *
  49. * The function uses a locking mechanism to prevent concurrent execution and handles
  50. * partial content blocks during streaming. It's designed to work with the streaming
  51. * API response pattern, where content arrives incrementally and needs to be processed
  52. * as it becomes available.
  53. */
  54. export async function presentAssistantMessage(cline: Task) {
  55. if (cline.abort) {
  56. throw new Error(`[Task#presentAssistantMessage] task ${cline.taskId}.${cline.instanceId} aborted`)
  57. }
  58. if (cline.presentAssistantMessageLocked) {
  59. cline.presentAssistantMessageHasPendingUpdates = true
  60. return
  61. }
  62. cline.presentAssistantMessageLocked = true
  63. cline.presentAssistantMessageHasPendingUpdates = false
  64. if (cline.currentStreamingContentIndex >= cline.assistantMessageContent.length) {
  65. // This may happen if the last content block was completed before
  66. // streaming could finish. If streaming is finished, and we're out of
  67. // bounds then this means we already presented/executed the last
  68. // content block and are ready to continue to next request.
  69. if (cline.didCompleteReadingStream) {
  70. cline.userMessageContentReady = true
  71. }
  72. cline.presentAssistantMessageLocked = false
  73. return
  74. }
  75. let block: any
  76. try {
  77. 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
  78. } catch (error) {
  79. console.error(`ERROR cloning block:`, error)
  80. console.error(
  81. `Block content:`,
  82. JSON.stringify(cline.assistantMessageContent[cline.currentStreamingContentIndex], null, 2),
  83. )
  84. cline.presentAssistantMessageLocked = false
  85. return
  86. }
  87. switch (block.type) {
  88. case "text": {
  89. if (cline.didRejectTool || cline.didAlreadyUseTool) {
  90. break
  91. }
  92. let content = block.content
  93. if (content) {
  94. // Have to do this for partial and complete since sending
  95. // content in thinking tags to markdown renderer will
  96. // automatically be removed.
  97. // Remove end substrings of <thinking or </thinking (below xml
  98. // parsing is only for opening tags).
  99. // Tthis is done with the xml parsing below now, but keeping
  100. // here for reference.
  101. // content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?$/, "")
  102. //
  103. // Remove all instances of <thinking> (with optional line break
  104. // after) and </thinking> (with optional line break before).
  105. // - Needs to be separate since we dont want to remove the line
  106. // break before the first tag.
  107. // - Needs to happen before the xml parsing below.
  108. content = content.replace(/<thinking>\s?/g, "")
  109. content = content.replace(/\s?<\/thinking>/g, "")
  110. // Remove partial XML tag at the very end of the content (for
  111. // tool use and thinking tags), Prevents scrollview from
  112. // jumping when tags are automatically removed.
  113. const lastOpenBracketIndex = content.lastIndexOf("<")
  114. if (lastOpenBracketIndex !== -1) {
  115. const possibleTag = content.slice(lastOpenBracketIndex)
  116. // Check if there's a '>' after the last '<' (i.e., if the
  117. // tag is complete) (complete thinking and tool tags will
  118. // have been removed by now.)
  119. const hasCloseBracket = possibleTag.includes(">")
  120. if (!hasCloseBracket) {
  121. // Extract the potential tag name.
  122. let tagContent: string
  123. if (possibleTag.startsWith("</")) {
  124. tagContent = possibleTag.slice(2).trim()
  125. } else {
  126. tagContent = possibleTag.slice(1).trim()
  127. }
  128. // Check if tagContent is likely an incomplete tag name
  129. // (letters and underscores only).
  130. const isLikelyTagName = /^[a-zA-Z_]+$/.test(tagContent)
  131. // Preemptively remove < or </ to keep from these
  132. // artifacts showing up in chat (also handles closing
  133. // thinking tags).
  134. const isOpeningOrClosing = possibleTag === "<" || possibleTag === "</"
  135. // If the tag is incomplete and at the end, remove it
  136. // from the content.
  137. if (isOpeningOrClosing || isLikelyTagName) {
  138. content = content.slice(0, lastOpenBracketIndex).trim()
  139. }
  140. }
  141. }
  142. }
  143. await cline.say("text", content, undefined, block.partial)
  144. break
  145. }
  146. case "tool_use":
  147. const toolDescription = (): string => {
  148. switch (block.name) {
  149. case "execute_command":
  150. return `[${block.name} for '${block.params.command}']`
  151. case "read_file":
  152. // Check if this model should use the simplified description
  153. const modelId = cline.api.getModel().id
  154. if (shouldUseSingleFileRead(modelId)) {
  155. return getSimpleReadFileToolDescription(block.name, block.params)
  156. } else {
  157. // Prefer native typed args when available; fall back to legacy params
  158. // Check if nativeArgs exists and is an array (native protocol)
  159. if (Array.isArray(block.nativeArgs)) {
  160. return readFileTool.getReadFileToolDescription(block.name, block.nativeArgs)
  161. }
  162. return readFileTool.getReadFileToolDescription(block.name, block.params)
  163. }
  164. case "fetch_instructions":
  165. return `[${block.name} for '${block.params.task}']`
  166. case "write_to_file":
  167. return `[${block.name} for '${block.params.path}']`
  168. case "apply_diff":
  169. // Handle both legacy format and new multi-file format
  170. if (block.params.path) {
  171. return `[${block.name} for '${block.params.path}']`
  172. } else if (block.params.args) {
  173. // Try to extract first file path from args for display
  174. const match = block.params.args.match(/<file>.*?<path>([^<]+)<\/path>/s)
  175. if (match) {
  176. const firstPath = match[1]
  177. // Check if there are multiple files
  178. const fileCount = (block.params.args.match(/<file>/g) || []).length
  179. if (fileCount > 1) {
  180. return `[${block.name} for '${firstPath}' and ${fileCount - 1} more file${fileCount > 2 ? "s" : ""}]`
  181. } else {
  182. return `[${block.name} for '${firstPath}']`
  183. }
  184. }
  185. }
  186. return `[${block.name}]`
  187. case "search_files":
  188. return `[${block.name} for '${block.params.regex}'${
  189. block.params.file_pattern ? ` in '${block.params.file_pattern}'` : ""
  190. }]`
  191. case "insert_content":
  192. return `[${block.name} for '${block.params.path}']`
  193. case "list_files":
  194. return `[${block.name} for '${block.params.path}']`
  195. case "list_code_definition_names":
  196. return `[${block.name} for '${block.params.path}']`
  197. case "browser_action":
  198. return `[${block.name} for '${block.params.action}']`
  199. case "use_mcp_tool":
  200. return `[${block.name} for '${block.params.server_name}']`
  201. case "access_mcp_resource":
  202. return `[${block.name} for '${block.params.server_name}']`
  203. case "ask_followup_question":
  204. return `[${block.name} for '${block.params.question}']`
  205. case "attempt_completion":
  206. return `[${block.name}]`
  207. case "switch_mode":
  208. return `[${block.name} to '${block.params.mode_slug}'${block.params.reason ? ` because: ${block.params.reason}` : ""}]`
  209. case "codebase_search": // Add case for the new tool
  210. return `[${block.name} for '${block.params.query}']`
  211. case "update_todo_list":
  212. return `[${block.name}]`
  213. case "new_task": {
  214. const mode = block.params.mode ?? defaultModeSlug
  215. const message = block.params.message ?? "(no message)"
  216. const modeName = getModeBySlug(mode, customModes)?.name ?? mode
  217. return `[${block.name} in ${modeName} mode: '${message}']`
  218. }
  219. case "run_slash_command":
  220. return `[${block.name} for '${block.params.command}'${block.params.args ? ` with args: ${block.params.args}` : ""}]`
  221. case "generate_image":
  222. return `[${block.name} for '${block.params.path}']`
  223. default:
  224. return `[${block.name}]`
  225. }
  226. }
  227. if (cline.didRejectTool) {
  228. // Ignore any tool content after user has rejected tool once.
  229. if (!block.partial) {
  230. cline.userMessageContent.push({
  231. type: "text",
  232. text: `Skipping tool ${toolDescription()} due to user rejecting a previous tool.`,
  233. })
  234. } else {
  235. // Partial tool after user rejected a previous tool.
  236. cline.userMessageContent.push({
  237. type: "text",
  238. text: `Tool ${toolDescription()} was interrupted and not executed due to user rejecting a previous tool.`,
  239. })
  240. }
  241. break
  242. }
  243. if (cline.didAlreadyUseTool) {
  244. // Ignore any content after a tool has already been used.
  245. cline.userMessageContent.push({
  246. type: "text",
  247. text: `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.`,
  248. })
  249. break
  250. }
  251. const pushToolResult = (content: ToolResponse) => {
  252. // Check if we're using native tool protocol
  253. const toolProtocol = vscode.workspace
  254. .getConfiguration(Package.name)
  255. .get<ToolProtocol>("toolProtocol", "xml")
  256. const isNative = isNativeProtocol(toolProtocol)
  257. // Get the tool call ID if this is a native tool call
  258. const toolCallId = (block as any).id
  259. if (isNative && toolCallId) {
  260. // For native protocol, add as tool_result block
  261. let resultContent: string
  262. if (typeof content === "string") {
  263. resultContent = content || "(tool did not return anything)"
  264. } else {
  265. // Convert array of content blocks to string for tool result
  266. // Tool results in OpenAI format only support strings
  267. resultContent = content
  268. .map((item) => {
  269. if (item.type === "text") {
  270. return item.text
  271. } else if (item.type === "image") {
  272. return "(image content)"
  273. }
  274. return ""
  275. })
  276. .join("\n")
  277. }
  278. cline.userMessageContent.push({
  279. type: "tool_result",
  280. tool_use_id: toolCallId,
  281. content: resultContent,
  282. } as Anthropic.ToolResultBlockParam)
  283. } else {
  284. // For XML protocol, add as text blocks (legacy behavior)
  285. cline.userMessageContent.push({ type: "text", text: `${toolDescription()} Result:` })
  286. if (typeof content === "string") {
  287. cline.userMessageContent.push({
  288. type: "text",
  289. text: content || "(tool did not return anything)",
  290. })
  291. } else {
  292. cline.userMessageContent.push(...content)
  293. }
  294. }
  295. // Once a tool result has been collected, ignore all other tool
  296. // uses since we should only ever present one tool result per
  297. // message.
  298. cline.didAlreadyUseTool = true
  299. }
  300. const askApproval = async (
  301. type: ClineAsk,
  302. partialMessage?: string,
  303. progressStatus?: ToolProgressStatus,
  304. isProtected?: boolean,
  305. ) => {
  306. const { response, text, images } = await cline.ask(
  307. type,
  308. partialMessage,
  309. false,
  310. progressStatus,
  311. isProtected || false,
  312. )
  313. if (response !== "yesButtonClicked") {
  314. // Handle both messageResponse and noButtonClicked with text.
  315. if (text) {
  316. await cline.say("user_feedback", text, images)
  317. pushToolResult(formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images))
  318. } else {
  319. pushToolResult(formatResponse.toolDenied())
  320. }
  321. cline.didRejectTool = true
  322. return false
  323. }
  324. // Handle yesButtonClicked with text.
  325. if (text) {
  326. await cline.say("user_feedback", text, images)
  327. pushToolResult(formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), images))
  328. }
  329. return true
  330. }
  331. const askFinishSubTaskApproval = async () => {
  332. // Ask the user to approve this task has completed, and he has
  333. // reviewed it, and we can declare task is finished and return
  334. // control to the parent task to continue running the rest of
  335. // the sub-tasks.
  336. const toolMessage = JSON.stringify({ tool: "finishTask" })
  337. return await askApproval("tool", toolMessage)
  338. }
  339. const handleError = async (action: string, error: Error) => {
  340. const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}`
  341. await cline.say(
  342. "error",
  343. `Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
  344. )
  345. pushToolResult(formatResponse.toolError(errorString))
  346. }
  347. // If block is partial, remove partial closing tag so its not
  348. // presented to user.
  349. const removeClosingTag = (tag: ToolParamName, text?: string): string => {
  350. if (!block.partial) {
  351. return text || ""
  352. }
  353. if (!text) {
  354. return ""
  355. }
  356. // This regex dynamically constructs a pattern to match the
  357. // closing tag:
  358. // - Optionally matches whitespace before the tag.
  359. // - Matches '<' or '</' optionally followed by any subset of
  360. // characters from the tag name.
  361. const tagRegex = new RegExp(
  362. `\\s?<\/?${tag
  363. .split("")
  364. .map((char) => `(?:${char})?`)
  365. .join("")}$`,
  366. "g",
  367. )
  368. return text.replace(tagRegex, "")
  369. }
  370. if (block.name !== "browser_action") {
  371. await cline.browserSession.closeBrowser()
  372. }
  373. if (!block.partial) {
  374. cline.recordToolUsage(block.name)
  375. TelemetryService.instance.captureToolUsage(cline.taskId, block.name)
  376. }
  377. // Validate tool use before execution.
  378. const { mode, customModes } = (await cline.providerRef.deref()?.getState()) ?? {}
  379. try {
  380. validateToolUse(
  381. block.name as ToolName,
  382. mode ?? defaultModeSlug,
  383. customModes ?? [],
  384. { apply_diff: cline.diffEnabled },
  385. block.params,
  386. )
  387. } catch (error) {
  388. cline.consecutiveMistakeCount++
  389. pushToolResult(formatResponse.toolError(error.message))
  390. break
  391. }
  392. // Check for identical consecutive tool calls.
  393. if (!block.partial) {
  394. // Use the detector to check for repetition, passing the ToolUse
  395. // block directly.
  396. const repetitionCheck = cline.toolRepetitionDetector.check(block)
  397. // If execution is not allowed, notify user and break.
  398. if (!repetitionCheck.allowExecution && repetitionCheck.askUser) {
  399. // Handle repetition similar to mistake_limit_reached pattern.
  400. const { response, text, images } = await cline.ask(
  401. repetitionCheck.askUser.messageKey as ClineAsk,
  402. repetitionCheck.askUser.messageDetail.replace("{toolName}", block.name),
  403. )
  404. if (response === "messageResponse") {
  405. // Add user feedback to userContent.
  406. cline.userMessageContent.push(
  407. {
  408. type: "text" as const,
  409. text: `Tool repetition limit reached. User feedback: ${text}`,
  410. },
  411. ...formatResponse.imageBlocks(images),
  412. )
  413. // Add user feedback to chat.
  414. await cline.say("user_feedback", text, images)
  415. // Track tool repetition in telemetry.
  416. TelemetryService.instance.captureConsecutiveMistakeError(cline.taskId)
  417. }
  418. // Return tool result message about the repetition
  419. pushToolResult(
  420. formatResponse.toolError(
  421. `Tool call repetition limit reached for ${block.name}. Please try a different approach.`,
  422. ),
  423. )
  424. break
  425. }
  426. }
  427. switch (block.name) {
  428. case "write_to_file":
  429. await checkpointSaveAndMark(cline)
  430. await writeToFileTool.handle(cline, block as ToolUse<"write_to_file">, {
  431. askApproval,
  432. handleError,
  433. pushToolResult,
  434. removeClosingTag,
  435. })
  436. break
  437. case "update_todo_list":
  438. await updateTodoListTool.handle(cline, block as ToolUse<"update_todo_list">, {
  439. askApproval,
  440. handleError,
  441. pushToolResult,
  442. removeClosingTag,
  443. })
  444. break
  445. case "apply_diff": {
  446. await checkpointSaveAndMark(cline)
  447. // Check if native protocol is enabled - if so, always use single-file class-based tool
  448. const toolProtocol = vscode.workspace
  449. .getConfiguration(Package.name)
  450. .get<ToolProtocol>("toolProtocol", "xml")
  451. if (isNativeProtocol(toolProtocol)) {
  452. await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
  453. askApproval,
  454. handleError,
  455. pushToolResult,
  456. removeClosingTag,
  457. })
  458. break
  459. }
  460. // Get the provider and state to check experiment settings
  461. const provider = cline.providerRef.deref()
  462. let isMultiFileApplyDiffEnabled = false
  463. if (provider) {
  464. const state = await provider.getState()
  465. isMultiFileApplyDiffEnabled = experiments.isEnabled(
  466. state.experiments ?? {},
  467. EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF,
  468. )
  469. }
  470. if (isMultiFileApplyDiffEnabled) {
  471. await applyDiffTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
  472. } else {
  473. await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
  474. askApproval,
  475. handleError,
  476. pushToolResult,
  477. removeClosingTag,
  478. })
  479. }
  480. break
  481. }
  482. case "insert_content":
  483. await checkpointSaveAndMark(cline)
  484. await insertContentTool.handle(cline, block as ToolUse<"insert_content">, {
  485. askApproval,
  486. handleError,
  487. pushToolResult,
  488. removeClosingTag,
  489. })
  490. break
  491. case "read_file":
  492. // Check if this model should use the simplified single-file read tool
  493. const modelId = cline.api.getModel().id
  494. if (shouldUseSingleFileRead(modelId)) {
  495. await simpleReadFileTool(
  496. cline,
  497. block,
  498. askApproval,
  499. handleError,
  500. pushToolResult,
  501. removeClosingTag,
  502. )
  503. } else {
  504. // Type assertion is safe here because we're in the "read_file" case
  505. await readFileTool.handle(cline, block as ToolUse<"read_file">, {
  506. askApproval,
  507. handleError,
  508. pushToolResult,
  509. removeClosingTag,
  510. })
  511. }
  512. break
  513. case "fetch_instructions":
  514. await fetchInstructionsTool.handle(cline, block as ToolUse<"fetch_instructions">, {
  515. askApproval,
  516. handleError,
  517. pushToolResult,
  518. removeClosingTag,
  519. })
  520. break
  521. case "list_files":
  522. await listFilesTool.handle(cline, block as ToolUse<"list_files">, {
  523. askApproval,
  524. handleError,
  525. pushToolResult,
  526. removeClosingTag,
  527. })
  528. break
  529. case "codebase_search":
  530. await codebaseSearchTool.handle(cline, block as ToolUse<"codebase_search">, {
  531. askApproval,
  532. handleError,
  533. pushToolResult,
  534. removeClosingTag,
  535. })
  536. break
  537. case "list_code_definition_names":
  538. await listCodeDefinitionNamesTool.handle(cline, block as ToolUse<"list_code_definition_names">, {
  539. askApproval,
  540. handleError,
  541. pushToolResult,
  542. removeClosingTag,
  543. })
  544. break
  545. case "search_files":
  546. await searchFilesTool.handle(cline, block as ToolUse<"search_files">, {
  547. askApproval,
  548. handleError,
  549. pushToolResult,
  550. removeClosingTag,
  551. })
  552. break
  553. case "browser_action":
  554. await browserActionTool.handle(cline, block as ToolUse<"browser_action">, {
  555. askApproval,
  556. handleError,
  557. pushToolResult,
  558. removeClosingTag,
  559. })
  560. break
  561. case "execute_command":
  562. await executeCommandTool.handle(cline, block as ToolUse<"execute_command">, {
  563. askApproval,
  564. handleError,
  565. pushToolResult,
  566. removeClosingTag,
  567. })
  568. break
  569. case "use_mcp_tool":
  570. await useMcpToolTool.handle(cline, block as ToolUse<"use_mcp_tool">, {
  571. askApproval,
  572. handleError,
  573. pushToolResult,
  574. removeClosingTag,
  575. })
  576. break
  577. case "access_mcp_resource":
  578. await accessMcpResourceTool(
  579. cline,
  580. block,
  581. askApproval,
  582. handleError,
  583. pushToolResult,
  584. removeClosingTag,
  585. )
  586. break
  587. case "ask_followup_question":
  588. await askFollowupQuestionTool.handle(cline, block as ToolUse<"ask_followup_question">, {
  589. askApproval,
  590. handleError,
  591. pushToolResult,
  592. removeClosingTag,
  593. })
  594. break
  595. case "switch_mode":
  596. await switchModeTool.handle(cline, block as ToolUse<"switch_mode">, {
  597. askApproval,
  598. handleError,
  599. pushToolResult,
  600. removeClosingTag,
  601. })
  602. break
  603. case "new_task":
  604. await newTaskTool.handle(cline, block as ToolUse<"new_task">, {
  605. askApproval,
  606. handleError,
  607. pushToolResult,
  608. removeClosingTag,
  609. })
  610. break
  611. case "attempt_completion": {
  612. const completionCallbacks: AttemptCompletionCallbacks = {
  613. askApproval,
  614. handleError,
  615. pushToolResult,
  616. removeClosingTag,
  617. askFinishSubTaskApproval,
  618. toolDescription,
  619. }
  620. await attemptCompletionTool.handle(
  621. cline,
  622. block as ToolUse<"attempt_completion">,
  623. completionCallbacks,
  624. )
  625. break
  626. }
  627. case "run_slash_command":
  628. await runSlashCommandTool.handle(cline, block as ToolUse<"run_slash_command">, {
  629. askApproval,
  630. handleError,
  631. pushToolResult,
  632. removeClosingTag,
  633. })
  634. break
  635. case "generate_image":
  636. await checkpointSaveAndMark(cline)
  637. await generateImageTool.handle(cline, block as ToolUse<"generate_image">, {
  638. askApproval,
  639. handleError,
  640. pushToolResult,
  641. removeClosingTag,
  642. })
  643. break
  644. }
  645. break
  646. }
  647. // Seeing out of bounds is fine, it means that the next too call is being
  648. // built up and ready to add to assistantMessageContent to present.
  649. // When you see the UI inactive during this, it means that a tool is
  650. // breaking without presenting any UI. For example the write_to_file tool
  651. // was breaking when relpath was undefined, and for invalid relpath it never
  652. // presented UI.
  653. // This needs to be placed here, if not then calling
  654. // cline.presentAssistantMessage below would fail (sometimes) since it's
  655. // locked.
  656. cline.presentAssistantMessageLocked = false
  657. // NOTE: When tool is rejected, iterator stream is interrupted and it waits
  658. // for `userMessageContentReady` to be true. Future calls to present will
  659. // skip execution since `didRejectTool` and iterate until `contentIndex` is
  660. // set to message length and it sets userMessageContentReady to true itself
  661. // (instead of preemptively doing it in iterator).
  662. if (!block.partial || cline.didRejectTool || cline.didAlreadyUseTool) {
  663. // Block is finished streaming and executing.
  664. if (cline.currentStreamingContentIndex === cline.assistantMessageContent.length - 1) {
  665. // It's okay that we increment if !didCompleteReadingStream, it'll
  666. // just return because out of bounds and as streaming continues it
  667. // will call `presentAssitantMessage` if a new block is ready. If
  668. // streaming is finished then we set `userMessageContentReady` to
  669. // true when out of bounds. This gracefully allows the stream to
  670. // continue on and all potential content blocks be presented.
  671. // Last block is complete and it is finished executing
  672. cline.userMessageContentReady = true // Will allow `pWaitFor` to continue.
  673. }
  674. // Call next block if it exists (if not then read stream will call it
  675. // when it's ready).
  676. // Need to increment regardless, so when read stream calls this function
  677. // again it will be streaming the next block.
  678. cline.currentStreamingContentIndex++
  679. if (cline.currentStreamingContentIndex < cline.assistantMessageContent.length) {
  680. // There are already more content blocks to stream, so we'll call
  681. // this function ourselves.
  682. presentAssistantMessage(cline)
  683. return
  684. } else {
  685. // CRITICAL FIX: If we're out of bounds and the stream is complete, set userMessageContentReady
  686. // This handles the case where assistantMessageContent is empty or becomes empty after processing
  687. if (cline.didCompleteReadingStream) {
  688. cline.userMessageContentReady = true
  689. }
  690. }
  691. }
  692. // Block is partial, but the read stream may have finished.
  693. if (cline.presentAssistantMessageHasPendingUpdates) {
  694. presentAssistantMessage(cline)
  695. }
  696. }
  697. /**
  698. * save checkpoint and mark done in the current streaming task.
  699. * @param task The Task instance to checkpoint save and mark.
  700. * @returns
  701. */
  702. async function checkpointSaveAndMark(task: Task) {
  703. if (task.currentStreamingDidCheckpoint) {
  704. return
  705. }
  706. try {
  707. await task.checkpointSave(true)
  708. task.currentStreamingDidCheckpoint = true
  709. } catch (error) {
  710. console.error(`[Task#presentAssistantMessage] Error saving checkpoint: ${error.message}`, error)
  711. }
  712. }