| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780 |
- 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 } 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 } 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 * as vscode from "vscode"
- import { ToolProtocol, isNativeProtocol } from "@roo-code/types"
- /**
- * 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 "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 <thinking or </thinking (below xml
- // parsing is only for opening tags).
- // Tthis is done with the xml parsing below now, but keeping
- // here for reference.
- // content = content.replace(/<\/?t(?:h(?:i(?:n(?:k(?:i(?:n(?:g)?)?)?$/, "")
- //
- // Remove all instances of <thinking> (with optional line break
- // after) and </thinking> (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(/<thinking>\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("</")) {
- tagContent = possibleTag.slice(2).trim()
- } else {
- tagContent = possibleTag.slice(1).trim()
- }
- // Check if tagContent is likely an incomplete tag name
- // (letters and underscores only).
- const isLikelyTagName = /^[a-zA-Z_]+$/.test(tagContent)
- // Preemptively remove < or </ to keep from these
- // artifacts showing up in chat (also handles closing
- // thinking tags).
- const isOpeningOrClosing = possibleTag === "<" || possibleTag === "</"
- // If the tag is incomplete and at the end, remove it
- // from the content.
- if (isOpeningOrClosing || isLikelyTagName) {
- content = content.slice(0, lastOpenBracketIndex).trim()
- }
- }
- }
- }
- await cline.say("text", content, undefined, block.partial)
- break
- }
- case "tool_use":
- const toolDescription = (): string => {
- 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 and is an array (native protocol)
- if (Array.isArray(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(/<file>.*?<path>([^<]+)<\/path>/s)
- if (match) {
- const firstPath = match[1]
- // Check if there are multiple files
- const fileCount = (block.params.args.match(/<file>/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.
- if (!block.partial) {
- cline.userMessageContent.push({
- type: "text",
- text: `Skipping tool ${toolDescription()} due to user rejecting a previous tool.`,
- })
- } else {
- // Partial tool after user rejected a previous tool.
- cline.userMessageContent.push({
- type: "text",
- text: `Tool ${toolDescription()} was interrupted and not executed due to user rejecting a previous tool.`,
- })
- }
- break
- }
- if (cline.didAlreadyUseTool) {
- // Ignore any content after a tool has already been used.
- cline.userMessageContent.push({
- type: "text",
- 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.`,
- })
- break
- }
- const pushToolResult = (content: ToolResponse) => {
- // Check if we're using native tool protocol
- const toolProtocol = vscode.workspace
- .getConfiguration(Package.name)
- .get<ToolProtocol>("toolProtocol", "xml")
- const isNative = isNativeProtocol(toolProtocol)
- // Get the tool call ID if this is a native tool call
- const toolCallId = (block as any).id
- if (isNative && toolCallId) {
- // For native protocol, add as tool_result block
- let resultContent: string
- if (typeof content === "string") {
- resultContent = content || "(tool did not return anything)"
- } else {
- // Convert array of content blocks to string for tool result
- // Tool results in OpenAI format only support strings
- resultContent = content
- .map((item) => {
- if (item.type === "text") {
- return item.text
- } else if (item.type === "image") {
- return "(image content)"
- }
- return ""
- })
- .join("\n")
- }
- cline.userMessageContent.push({
- type: "tool_result",
- tool_use_id: toolCallId,
- content: resultContent,
- } as Anthropic.ToolResultBlockParam)
- } 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)
- }
- }
- // Once a tool result has been collected, ignore all other tool
- // uses since we should only ever present one tool result per
- // message.
- cline.didAlreadyUseTool = true
- }
- 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), images))
- } else {
- pushToolResult(formatResponse.toolDenied())
- }
- cline.didRejectTool = true
- return false
- }
- // Handle yesButtonClicked with text.
- if (text) {
- await cline.say("user_feedback", text, images)
- pushToolResult(formatResponse.toolResult(formatResponse.toolApprovedWithFeedback(text), 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))
- }
- // 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 '</' optionally followed by any subset of
- // characters from the tag name.
- const tagRegex = new RegExp(
- `\\s?<\/?${tag
- .split("")
- .map((char) => `(?:${char})?`)
- .join("")}$`,
- "g",
- )
- return text.replace(tagRegex, "")
- }
- if (block.name !== "browser_action") {
- await cline.browserSession.closeBrowser()
- }
- if (!block.partial) {
- cline.recordToolUsage(block.name)
- TelemetryService.instance.captureToolUsage(cline.taskId, block.name)
- }
- // 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))
- 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.`,
- ),
- )
- 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,
- })
- break
- case "update_todo_list":
- await updateTodoListTool.handle(cline, block as ToolUse<"update_todo_list">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- })
- break
- case "apply_diff": {
- await checkpointSaveAndMark(cline)
- // Check if native protocol is enabled - if so, always use single-file class-based tool
- const toolProtocol = vscode.workspace
- .getConfiguration(Package.name)
- .get<ToolProtocol>("toolProtocol", "xml")
- if (isNativeProtocol(toolProtocol)) {
- await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- })
- 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,
- })
- }
- break
- }
- case "insert_content":
- await checkpointSaveAndMark(cline)
- await insertContentTool.handle(cline, block as ToolUse<"insert_content">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- })
- 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,
- )
- } 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,
- })
- }
- break
- case "fetch_instructions":
- await fetchInstructionsTool.handle(cline, block as ToolUse<"fetch_instructions">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- })
- break
- case "list_files":
- await listFilesTool.handle(cline, block as ToolUse<"list_files">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- })
- break
- case "codebase_search":
- await codebaseSearchTool.handle(cline, block as ToolUse<"codebase_search">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- })
- break
- case "list_code_definition_names":
- await listCodeDefinitionNamesTool.handle(cline, block as ToolUse<"list_code_definition_names">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- })
- break
- case "search_files":
- await searchFilesTool.handle(cline, block as ToolUse<"search_files">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- })
- break
- case "browser_action":
- await browserActionTool.handle(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,
- })
- break
- case "use_mcp_tool":
- await useMcpToolTool.handle(cline, block as ToolUse<"use_mcp_tool">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- })
- break
- case "access_mcp_resource":
- await accessMcpResourceTool(
- cline,
- block,
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- )
- break
- case "ask_followup_question":
- await askFollowupQuestionTool.handle(cline, block as ToolUse<"ask_followup_question">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- })
- break
- case "switch_mode":
- await switchModeTool.handle(cline, block as ToolUse<"switch_mode">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- })
- break
- case "new_task":
- await newTaskTool.handle(cline, block as ToolUse<"new_task">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- })
- break
- case "attempt_completion": {
- const completionCallbacks: AttemptCompletionCallbacks = {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- askFinishSubTaskApproval,
- toolDescription,
- }
- 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,
- })
- break
- case "generate_image":
- await checkpointSaveAndMark(cline)
- await generateImageTool.handle(cline, block as ToolUse<"generate_image">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- })
- 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)
- }
- }
|