diff --git a/src/core/Cline.ts b/src/core/Cline.ts index ba171ce3fa1..5bc280dc423 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -48,6 +48,7 @@ import { ClineSay, ClineSayBrowserAction, ClineSayTool, + ToolProgressStatus, } from "../shared/ExtensionMessage" import { getApiMetrics } from "../shared/getApiMetrics" import { HistoryItem } from "../shared/HistoryItem" @@ -408,6 +409,7 @@ export class Cline { type: ClineAsk, text?: string, partial?: boolean, + progressStatus?: ToolProgressStatus, ): Promise<{ response: ClineAskResponse; text?: string; images?: string[] }> { // If this Cline instance was aborted by the provider, then the only thing keeping us alive is a promise still running in the background, in which case we don't want to send its result to the webview as it is attached to a new instance of Cline now. So we can safely ignore the result of any active promises, and this class will be deallocated. (Although we set Cline = undefined in provider, that simply removes the reference to this instance, but the instance is still alive until this promise resolves or rejects.) if (this.abort) { @@ -423,6 +425,7 @@ export class Cline { // existing partial message, so update it lastMessage.text = text lastMessage.partial = partial + lastMessage.progressStatus = progressStatus // todo be more efficient about saving and posting only new data or one whole message at a time so ignore partial for saves, and only post parts of partial message instead of whole array in new listener // await this.saveClineMessages() // await this.providerRef.deref()?.postStateToWebview() @@ -460,6 +463,8 @@ export class Cline { // lastMessage.ts = askTs lastMessage.text = text lastMessage.partial = false + lastMessage.progressStatus = progressStatus + await this.saveClineMessages() // await this.providerRef.deref()?.postStateToWebview() await this.providerRef @@ -511,6 +516,7 @@ export class Cline { images?: string[], partial?: boolean, checkpoint?: Record, + progressStatus?: ToolProgressStatus, ): Promise { if (this.abort) { throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#2)`) @@ -526,6 +532,7 @@ export class Cline { lastMessage.text = text lastMessage.images = images lastMessage.partial = partial + lastMessage.progressStatus = progressStatus await this.providerRef .deref() ?.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage }) @@ -545,6 +552,7 @@ export class Cline { lastMessage.text = text lastMessage.images = images lastMessage.partial = false + lastMessage.progressStatus = progressStatus // instead of streaming partialMessage events, we do a save and post like normal to persist to disk await this.saveClineMessages() @@ -1394,8 +1402,12 @@ export class Cline { isCheckpointPossible = true } - const askApproval = async (type: ClineAsk, partialMessage?: string) => { - const { response, text, images } = await this.ask(type, partialMessage, false) + const askApproval = async ( + type: ClineAsk, + partialMessage?: string, + progressStatus?: ToolProgressStatus, + ) => { + const { response, text, images } = await this.ask(type, partialMessage, false, progressStatus) if (response !== "yesButtonClicked") { // Handle both messageResponse and noButtonClicked with text if (text) { @@ -1703,8 +1715,16 @@ export class Cline { try { if (block.partial) { // update gui message + let toolProgressStatus + if (this.diffStrategy && this.diffStrategy.getProgressStatus) { + toolProgressStatus = this.diffStrategy.getProgressStatus(block) + } + const partialMessage = JSON.stringify(sharedMessageProps) - await this.ask("tool", partialMessage, block.partial).catch(() => {}) + + await this.ask("tool", partialMessage, block.partial, toolProgressStatus).catch( + () => {}, + ) break } else { if (!relPath) { @@ -1799,7 +1819,12 @@ export class Cline { diff: diffContent, } satisfies ClineSayTool) - const didApprove = await askApproval("tool", completeMessage) + let toolProgressStatus + if (this.diffStrategy && this.diffStrategy.getProgressStatus) { + toolProgressStatus = this.diffStrategy.getProgressStatus(block, diffResult) + } + + const didApprove = await askApproval("tool", completeMessage, toolProgressStatus) if (!didApprove) { await this.diffViewProvider.revertChanges() // This likely handles closing the diff view break diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts index 99c22a31df9..bcf2f654308 100644 --- a/src/core/diff/strategies/multi-search-replace.ts +++ b/src/core/diff/strategies/multi-search-replace.ts @@ -1,6 +1,8 @@ import { DiffStrategy, DiffResult } from "../types" import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text" import { distance } from "fastest-levenshtein" +import { ToolProgressStatus } from "../../../shared/ExtensionMessage" +import { ToolUse } from "../../assistant-message" const BUFFER_LINES = 40 // Number of extra context lines to show before and after matches @@ -362,4 +364,27 @@ Only use a single line of '=======' between search and replacement content, beca failParts: diffResults, } } + + getProgressStatus(toolUse: ToolUse, result?: DiffResult): ToolProgressStatus { + const diffContent = toolUse.params.diff + if (diffContent) { + const icon = "diff-multiple" + const searchBlockCount = (diffContent.match(/SEARCH/g) || []).length + if (toolUse.partial) { + if (diffContent.length < 1000 || (diffContent.length / 50) % 10 === 0) { + return { icon, text: `${searchBlockCount}` } + } + } else if (result) { + if (result.failParts?.length) { + return { + icon, + text: `${searchBlockCount - result.failParts.length}/${searchBlockCount}`, + } + } else { + return { icon, text: `${searchBlockCount}` } + } + } + } + return {} + } } diff --git a/src/core/diff/types.ts b/src/core/diff/types.ts index be6d8cd3110..e12a47762d5 100644 --- a/src/core/diff/types.ts +++ b/src/core/diff/types.ts @@ -2,6 +2,9 @@ * Interface for implementing different diff strategies */ +import { ToolProgressStatus } from "../../shared/ExtensionMessage" +import { ToolUse } from "../assistant-message" + export type DiffResult = | { success: true; content: string; failParts?: DiffResult[] } | ({ @@ -34,4 +37,6 @@ export interface DiffStrategy { * @returns A DiffResult object containing either the successful result or error details */ applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): Promise + + getProgressStatus?(toolUse: ToolUse, result?: any): ToolProgressStatus } diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index 2004b31e8ba..e310fef6a7a 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -92,6 +92,7 @@ export interface ClineMessage { reasoning?: string conversationHistoryIndex?: number checkpoint?: Record + progressStatus?: ToolProgressStatus } export interface ClineProvider { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 33c485adffd..9298b0cb1b9 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -231,3 +231,8 @@ export interface HumanRelayCancelMessage { } export type ClineApiReqCancelReason = "streaming_failed" | "user_cancelled" + +export type ToolProgressStatus = { + icon?: string + text?: string +} diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 259c03fa213..b19d67dc052 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -258,6 +258,7 @@ export const ChatRowContent = ({ Roo wants to edit this file: void isLoading?: boolean + progressStatus?: ToolProgressStatus } /* @@ -32,6 +34,7 @@ const CodeAccordian = ({ isExpanded, onToggleExpand, isLoading, + progressStatus, }: CodeAccordianProps) => { const inferredLanguage = useMemo( () => code && (language ?? (path ? getLanguageFromPath(path) : undefined)), @@ -95,6 +98,14 @@ const CodeAccordian = ({ )}
+ {progressStatus && progressStatus.text && ( + <> + {progressStatus.icon && } + + {progressStatus.text} + + + )} )}