|
@@ -59,6 +59,7 @@ import { calculateApiCost } from "../utils/cost"
|
|
|
import { fileExistsAtPath } from "../utils/fs"
|
|
import { fileExistsAtPath } from "../utils/fs"
|
|
|
import { arePathsEqual, getReadablePath } from "../utils/path"
|
|
import { arePathsEqual, getReadablePath } from "../utils/path"
|
|
|
import { parseMentions } from "./mentions"
|
|
import { parseMentions } from "./mentions"
|
|
|
|
|
+import { RooIgnoreController, LOCK_TEXT_SYMBOL } from "./ignore/RooIgnoreController"
|
|
|
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
|
|
import { AssistantMessageContent, parseAssistantMessage, ToolParamName, ToolUseName } from "./assistant-message"
|
|
|
import { formatResponse } from "./prompts/responses"
|
|
import { formatResponse } from "./prompts/responses"
|
|
|
import { SYSTEM_PROMPT } from "./prompts/system"
|
|
import { SYSTEM_PROMPT } from "./prompts/system"
|
|
@@ -94,6 +95,17 @@ export type ClineOptions = {
|
|
|
|
|
|
|
|
export class Cline {
|
|
export class Cline {
|
|
|
readonly taskId: string
|
|
readonly taskId: string
|
|
|
|
|
+ private taskNumber: number
|
|
|
|
|
+ // a flag that indicated if this Cline instance is a subtask (on finish return control to parent task)
|
|
|
|
|
+ private isSubTask: boolean = false
|
|
|
|
|
+ // a flag that indicated if this Cline instance is paused (waiting for provider to resume it after subtask completion)
|
|
|
|
|
+ private isPaused: boolean = false
|
|
|
|
|
+ // this is the parent task work mode when it launched the subtask to be used when it is restored (so the last used mode by parent task will also be restored)
|
|
|
|
|
+ private pausedModeSlug: string = defaultModeSlug
|
|
|
|
|
+ // if this is a subtask then this member holds a pointer to the parent task that launched it
|
|
|
|
|
+ private parentTask: Cline | undefined = undefined
|
|
|
|
|
+ // if this is a subtask then this member holds a pointer to the top parent task that launched it
|
|
|
|
|
+ private rootTask: Cline | undefined = undefined
|
|
|
readonly apiConfiguration: ApiConfiguration
|
|
readonly apiConfiguration: ApiConfiguration
|
|
|
api: ApiHandler
|
|
api: ApiHandler
|
|
|
private terminalManager: TerminalManager
|
|
private terminalManager: TerminalManager
|
|
@@ -107,6 +119,7 @@ export class Cline {
|
|
|
|
|
|
|
|
apiConversationHistory: (Anthropic.MessageParam & { ts?: number })[] = []
|
|
apiConversationHistory: (Anthropic.MessageParam & { ts?: number })[] = []
|
|
|
clineMessages: ClineMessage[] = []
|
|
clineMessages: ClineMessage[] = []
|
|
|
|
|
+ rooIgnoreController?: RooIgnoreController
|
|
|
private askResponse?: ClineAskResponse
|
|
private askResponse?: ClineAskResponse
|
|
|
private askResponseText?: string
|
|
private askResponseText?: string
|
|
|
private askResponseImages?: string[]
|
|
private askResponseImages?: string[]
|
|
@@ -157,8 +170,13 @@ export class Cline {
|
|
|
throw new Error("Either historyItem or task/images must be provided")
|
|
throw new Error("Either historyItem or task/images must be provided")
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- this.taskId = historyItem ? historyItem.id : crypto.randomUUID()
|
|
|
|
|
|
|
+ this.rooIgnoreController = new RooIgnoreController(cwd)
|
|
|
|
|
+ this.rooIgnoreController.initialize().catch((error) => {
|
|
|
|
|
+ console.error("Failed to initialize RooIgnoreController:", error)
|
|
|
|
|
+ })
|
|
|
|
|
|
|
|
|
|
+ this.taskId = historyItem ? historyItem.id : crypto.randomUUID()
|
|
|
|
|
+ this.taskNumber = -1
|
|
|
this.apiConfiguration = apiConfiguration
|
|
this.apiConfiguration = apiConfiguration
|
|
|
this.api = buildApiHandler(apiConfiguration)
|
|
this.api = buildApiHandler(apiConfiguration)
|
|
|
this.terminalManager = new TerminalManager()
|
|
this.terminalManager = new TerminalManager()
|
|
@@ -173,7 +191,10 @@ export class Cline {
|
|
|
this.checkpointStorage = checkpointStorage
|
|
this.checkpointStorage = checkpointStorage
|
|
|
|
|
|
|
|
// Initialize diffStrategy based on current state
|
|
// Initialize diffStrategy based on current state
|
|
|
- this.updateDiffStrategy(Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.DIFF_STRATEGY))
|
|
|
|
|
|
|
+ this.updateDiffStrategy(
|
|
|
|
|
+ Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.DIFF_STRATEGY),
|
|
|
|
|
+ Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE),
|
|
|
|
|
+ )
|
|
|
|
|
|
|
|
if (startTask) {
|
|
if (startTask) {
|
|
|
if (task || images) {
|
|
if (task || images) {
|
|
@@ -202,14 +223,64 @@ export class Cline {
|
|
|
return [instance, promise]
|
|
return [instance, promise]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // a helper function to set the private member isSubTask to true
|
|
|
|
|
+ // and by that set this Cline instance to be a subtask (on finish return control to parent task)
|
|
|
|
|
+ setSubTask() {
|
|
|
|
|
+ this.isSubTask = true
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // sets the task number (sequencial number of this task from all the subtask ran from this main task stack)
|
|
|
|
|
+ setTaskNumber(taskNumber: number) {
|
|
|
|
|
+ this.taskNumber = taskNumber
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // gets the task number, the sequencial number of this task from all the subtask ran from this main task stack
|
|
|
|
|
+ getTaskNumber() {
|
|
|
|
|
+ return this.taskNumber
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // this method returns the cline instance that is the parent task that launched this subtask (assuming this cline is a subtask)
|
|
|
|
|
+ // if undefined is returned, then there is no parent task and this is not a subtask or connection has been severed
|
|
|
|
|
+ getParentTask(): Cline | undefined {
|
|
|
|
|
+ return this.parentTask
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // this method sets a cline instance that is the parent task that called this task (assuming this cline is a subtask)
|
|
|
|
|
+ // if undefined is set, then the connection is broken and the parent is no longer saved in the subtask member
|
|
|
|
|
+ setParentTask(parentToSet: Cline | undefined) {
|
|
|
|
|
+ this.parentTask = parentToSet
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // this method returns the cline instance that is the root task (top most parent) that eventually launched this subtask (assuming this cline is a subtask)
|
|
|
|
|
+ // if undefined is returned, then there is no root task and this is not a subtask or connection has been severed
|
|
|
|
|
+ getRootTask(): Cline | undefined {
|
|
|
|
|
+ return this.rootTask
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // this method sets a cline instance that is the root task (top most patrnt) that called this task (assuming this cline is a subtask)
|
|
|
|
|
+ // if undefined is set, then the connection is broken and the root is no longer saved in the subtask member
|
|
|
|
|
+ setRootTask(rootToSet: Cline | undefined) {
|
|
|
|
|
+ this.rootTask = rootToSet
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Add method to update diffStrategy
|
|
// Add method to update diffStrategy
|
|
|
- async updateDiffStrategy(experimentalDiffStrategy?: boolean) {
|
|
|
|
|
|
|
+ async updateDiffStrategy(experimentalDiffStrategy?: boolean, multiSearchReplaceDiffStrategy?: boolean) {
|
|
|
// If not provided, get from current state
|
|
// If not provided, get from current state
|
|
|
- if (experimentalDiffStrategy === undefined) {
|
|
|
|
|
|
|
+ if (experimentalDiffStrategy === undefined || multiSearchReplaceDiffStrategy === undefined) {
|
|
|
const { experiments: stateExperimental } = (await this.providerRef.deref()?.getState()) ?? {}
|
|
const { experiments: stateExperimental } = (await this.providerRef.deref()?.getState()) ?? {}
|
|
|
- experimentalDiffStrategy = stateExperimental?.[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false
|
|
|
|
|
|
|
+ if (experimentalDiffStrategy === undefined) {
|
|
|
|
|
+ experimentalDiffStrategy = stateExperimental?.[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false
|
|
|
|
|
+ }
|
|
|
|
|
+ if (multiSearchReplaceDiffStrategy === undefined) {
|
|
|
|
|
+ multiSearchReplaceDiffStrategy = stateExperimental?.[EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE] ?? false
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- this.diffStrategy = getDiffStrategy(this.api.getModel().id, this.fuzzyMatchThreshold, experimentalDiffStrategy)
|
|
|
|
|
|
|
+ this.diffStrategy = getDiffStrategy(
|
|
|
|
|
+ this.api.getModel().id,
|
|
|
|
|
+ this.fuzzyMatchThreshold,
|
|
|
|
|
+ experimentalDiffStrategy,
|
|
|
|
|
+ multiSearchReplaceDiffStrategy,
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Storing task to disk for history
|
|
// Storing task to disk for history
|
|
@@ -308,6 +379,7 @@ export class Cline {
|
|
|
|
|
|
|
|
await this.providerRef.deref()?.updateTaskHistory({
|
|
await this.providerRef.deref()?.updateTaskHistory({
|
|
|
id: this.taskId,
|
|
id: this.taskId,
|
|
|
|
|
+ number: this.taskNumber,
|
|
|
ts: lastRelevantMessage.ts,
|
|
ts: lastRelevantMessage.ts,
|
|
|
task: taskMessage.text ?? "",
|
|
task: taskMessage.text ?? "",
|
|
|
tokensIn: apiMetrics.totalTokensIn,
|
|
tokensIn: apiMetrics.totalTokensIn,
|
|
@@ -332,7 +404,7 @@ export class Cline {
|
|
|
): Promise<{ response: ClineAskResponse; text?: string; images?: string[] }> {
|
|
): 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 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) {
|
|
if (this.abort) {
|
|
|
- throw new Error("Roo Code instance aborted")
|
|
|
|
|
|
|
+ throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#1)`)
|
|
|
}
|
|
}
|
|
|
let askTs: number
|
|
let askTs: number
|
|
|
if (partial !== undefined) {
|
|
if (partial !== undefined) {
|
|
@@ -350,7 +422,7 @@ export class Cline {
|
|
|
await this.providerRef
|
|
await this.providerRef
|
|
|
.deref()
|
|
.deref()
|
|
|
?.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage })
|
|
?.postMessageToWebview({ type: "partialMessage", partialMessage: lastMessage })
|
|
|
- throw new Error("Current ask promise was ignored 1")
|
|
|
|
|
|
|
+ throw new Error("Current ask promise was ignored (#1)")
|
|
|
} else {
|
|
} else {
|
|
|
// this is a new partial message, so add it with partial state
|
|
// this is a new partial message, so add it with partial state
|
|
|
// this.askResponse = undefined
|
|
// this.askResponse = undefined
|
|
@@ -360,7 +432,7 @@ export class Cline {
|
|
|
this.lastMessageTs = askTs
|
|
this.lastMessageTs = askTs
|
|
|
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, partial })
|
|
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, partial })
|
|
|
await this.providerRef.deref()?.postStateToWebview()
|
|
await this.providerRef.deref()?.postStateToWebview()
|
|
|
- throw new Error("Current ask promise was ignored 2")
|
|
|
|
|
|
|
+ throw new Error("Current ask promise was ignored (#2)")
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
// partial=false means its a complete version of a previously partial message
|
|
// partial=false means its a complete version of a previously partial message
|
|
@@ -434,7 +506,7 @@ export class Cline {
|
|
|
checkpoint?: Record<string, unknown>,
|
|
checkpoint?: Record<string, unknown>,
|
|
|
): Promise<undefined> {
|
|
): Promise<undefined> {
|
|
|
if (this.abort) {
|
|
if (this.abort) {
|
|
|
- throw new Error("Roo Code instance aborted")
|
|
|
|
|
|
|
+ throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#2)`)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (partial !== undefined) {
|
|
if (partial !== undefined) {
|
|
@@ -522,6 +594,32 @@ export class Cline {
|
|
|
])
|
|
])
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ async resumePausedTask(lastMessage?: string) {
|
|
|
|
|
+ // release this Cline instance from paused state
|
|
|
|
|
+ this.isPaused = false
|
|
|
|
|
+
|
|
|
|
|
+ // fake an answer from the subtask that it has completed running and this is the result of what it has done
|
|
|
|
|
+ // add the message to the chat history and to the webview ui
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.say("text", `${lastMessage ?? "Please continue to the next task."}`)
|
|
|
|
|
+
|
|
|
|
|
+ await this.addToApiConversationHistory({
|
|
|
|
|
+ role: "user",
|
|
|
|
|
+ content: [
|
|
|
|
|
+ {
|
|
|
|
|
+ type: "text",
|
|
|
|
|
+ text: `[new_task completed] Result: ${lastMessage ?? "Please continue to the next task."}`,
|
|
|
|
|
+ },
|
|
|
|
|
+ ],
|
|
|
|
|
+ })
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ this.providerRef
|
|
|
|
|
+ .deref()
|
|
|
|
|
+ ?.log(`Error failed to add reply from subtast into conversation of parent task, error: ${error}`)
|
|
|
|
|
+ throw error
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
private async resumeTaskFromHistory() {
|
|
private async resumeTaskFromHistory() {
|
|
|
const modifiedClineMessages = await this.getSavedClineMessages()
|
|
const modifiedClineMessages = await this.getSavedClineMessages()
|
|
|
|
|
|
|
@@ -802,6 +900,7 @@ export class Cline {
|
|
|
this.terminalManager.disposeAll()
|
|
this.terminalManager.disposeAll()
|
|
|
this.urlContentFetcher.closeBrowser()
|
|
this.urlContentFetcher.closeBrowser()
|
|
|
this.browserSession.closeBrowser()
|
|
this.browserSession.closeBrowser()
|
|
|
|
|
+ this.rooIgnoreController?.dispose()
|
|
|
|
|
|
|
|
// If we're not streaming then `abortStream` (which reverts the diff
|
|
// If we're not streaming then `abortStream` (which reverts the diff
|
|
|
// view changes) won't be called, so we need to revert the changes here.
|
|
// view changes) won't be called, so we need to revert the changes here.
|
|
@@ -953,6 +1052,8 @@ export class Cline {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const rooIgnoreInstructions = this.rooIgnoreController?.getInstructions()
|
|
|
|
|
+
|
|
|
const {
|
|
const {
|
|
|
browserViewportSize,
|
|
browserViewportSize,
|
|
|
mode,
|
|
mode,
|
|
@@ -983,6 +1084,7 @@ export class Cline {
|
|
|
this.diffEnabled,
|
|
this.diffEnabled,
|
|
|
experiments,
|
|
experiments,
|
|
|
enableMcpServerCreation,
|
|
enableMcpServerCreation,
|
|
|
|
|
+ rooIgnoreInstructions,
|
|
|
)
|
|
)
|
|
|
})()
|
|
})()
|
|
|
|
|
|
|
@@ -1105,7 +1207,7 @@ export class Cline {
|
|
|
|
|
|
|
|
async presentAssistantMessage() {
|
|
async presentAssistantMessage() {
|
|
|
if (this.abort) {
|
|
if (this.abort) {
|
|
|
- throw new Error("Roo Code instance aborted")
|
|
|
|
|
|
|
+ throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#3)`)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (this.presentAssistantMessageLocked) {
|
|
if (this.presentAssistantMessageLocked) {
|
|
@@ -1357,6 +1459,15 @@ export class Cline {
|
|
|
// wait so we can determine if it's a new file or editing an existing file
|
|
// wait so we can determine if it's a new file or editing an existing file
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ const accessAllowed = this.rooIgnoreController?.validateAccess(relPath)
|
|
|
|
|
+ if (!accessAllowed) {
|
|
|
|
|
+ await this.say("rooignore_error", relPath)
|
|
|
|
|
+ pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
|
|
|
|
|
+
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Check if file exists using cached map or fs.access
|
|
// Check if file exists using cached map or fs.access
|
|
|
let fileExists: boolean
|
|
let fileExists: boolean
|
|
|
if (this.diffViewProvider.editType !== undefined) {
|
|
if (this.diffViewProvider.editType !== undefined) {
|
|
@@ -1566,6 +1677,14 @@ export class Cline {
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const accessAllowed = this.rooIgnoreController?.validateAccess(relPath)
|
|
|
|
|
+ if (!accessAllowed) {
|
|
|
|
|
+ await this.say("rooignore_error", relPath)
|
|
|
|
|
+ pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
|
|
|
|
|
+
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const absolutePath = path.resolve(cwd, relPath)
|
|
const absolutePath = path.resolve(cwd, relPath)
|
|
|
const fileExists = await fileExistsAtPath(absolutePath)
|
|
const fileExists = await fileExistsAtPath(absolutePath)
|
|
|
|
|
|
|
@@ -1589,17 +1708,36 @@ export class Cline {
|
|
|
success: false,
|
|
success: false,
|
|
|
error: "No diff strategy available",
|
|
error: "No diff strategy available",
|
|
|
}
|
|
}
|
|
|
|
|
+ let partResults = ""
|
|
|
|
|
+
|
|
|
if (!diffResult.success) {
|
|
if (!diffResult.success) {
|
|
|
this.consecutiveMistakeCount++
|
|
this.consecutiveMistakeCount++
|
|
|
const currentCount =
|
|
const currentCount =
|
|
|
(this.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1
|
|
(this.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1
|
|
|
this.consecutiveMistakeCountForApplyDiff.set(relPath, currentCount)
|
|
this.consecutiveMistakeCountForApplyDiff.set(relPath, currentCount)
|
|
|
- const errorDetails = diffResult.details
|
|
|
|
|
- ? JSON.stringify(diffResult.details, null, 2)
|
|
|
|
|
- : ""
|
|
|
|
|
- const formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${
|
|
|
|
|
- diffResult.error
|
|
|
|
|
- }${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
|
|
|
|
|
|
|
+ let formattedError = ""
|
|
|
|
|
+ if (diffResult.failParts && diffResult.failParts.length > 0) {
|
|
|
|
|
+ for (const failPart of diffResult.failParts) {
|
|
|
|
|
+ if (failPart.success) {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ const errorDetails = failPart.details
|
|
|
|
|
+ ? JSON.stringify(failPart.details, null, 2)
|
|
|
|
|
+ : ""
|
|
|
|
|
+ formattedError = `<error_details>\n${
|
|
|
|
|
+ failPart.error
|
|
|
|
|
+ }${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
|
|
|
|
|
+ partResults += formattedError
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const errorDetails = diffResult.details
|
|
|
|
|
+ ? JSON.stringify(diffResult.details, null, 2)
|
|
|
|
|
+ : ""
|
|
|
|
|
+ formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${
|
|
|
|
|
+ diffResult.error
|
|
|
|
|
+ }${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (currentCount >= 2) {
|
|
if (currentCount >= 2) {
|
|
|
await this.say("error", formattedError)
|
|
await this.say("error", formattedError)
|
|
|
}
|
|
}
|
|
@@ -1629,6 +1767,10 @@ export class Cline {
|
|
|
const { newProblemsMessage, userEdits, finalContent } =
|
|
const { newProblemsMessage, userEdits, finalContent } =
|
|
|
await this.diffViewProvider.saveChanges()
|
|
await this.diffViewProvider.saveChanges()
|
|
|
this.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
|
|
this.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
|
|
|
|
|
+ let partFailHint = ""
|
|
|
|
|
+ if (diffResult.failParts && diffResult.failParts.length > 0) {
|
|
|
|
|
+ partFailHint = `But unable to apply all diff parts to file: ${absolutePath}, silently use <read_file> tool to check newest file version and re-apply diffs\n`
|
|
|
|
|
+ }
|
|
|
if (userEdits) {
|
|
if (userEdits) {
|
|
|
await this.say(
|
|
await this.say(
|
|
|
"user_feedback_diff",
|
|
"user_feedback_diff",
|
|
@@ -1640,6 +1782,7 @@ export class Cline {
|
|
|
)
|
|
)
|
|
|
pushToolResult(
|
|
pushToolResult(
|
|
|
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
|
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
|
|
|
|
+ partFailHint +
|
|
|
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
|
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
|
|
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(
|
|
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(
|
|
|
finalContent || "",
|
|
finalContent || "",
|
|
@@ -1652,7 +1795,8 @@ export class Cline {
|
|
|
)
|
|
)
|
|
|
} else {
|
|
} else {
|
|
|
pushToolResult(
|
|
pushToolResult(
|
|
|
- `Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}`,
|
|
|
|
|
|
|
+ `Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}\n` +
|
|
|
|
|
+ partFailHint,
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
await this.diffViewProvider.reset()
|
|
await this.diffViewProvider.reset()
|
|
@@ -1999,6 +2143,15 @@ export class Cline {
|
|
|
pushToolResult(await this.sayAndCreateMissingParamError("read_file", "path"))
|
|
pushToolResult(await this.sayAndCreateMissingParamError("read_file", "path"))
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ const accessAllowed = this.rooIgnoreController?.validateAccess(relPath)
|
|
|
|
|
+ if (!accessAllowed) {
|
|
|
|
|
+ await this.say("rooignore_error", relPath)
|
|
|
|
|
+ pushToolResult(formatResponse.toolError(formatResponse.rooIgnoreError(relPath)))
|
|
|
|
|
+
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
this.consecutiveMistakeCount = 0
|
|
this.consecutiveMistakeCount = 0
|
|
|
const absolutePath = path.resolve(cwd, relPath)
|
|
const absolutePath = path.resolve(cwd, relPath)
|
|
|
const completeMessage = JSON.stringify({
|
|
const completeMessage = JSON.stringify({
|
|
@@ -2044,7 +2197,12 @@ export class Cline {
|
|
|
this.consecutiveMistakeCount = 0
|
|
this.consecutiveMistakeCount = 0
|
|
|
const absolutePath = path.resolve(cwd, relDirPath)
|
|
const absolutePath = path.resolve(cwd, relDirPath)
|
|
|
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
|
|
const [files, didHitLimit] = await listFiles(absolutePath, recursive, 200)
|
|
|
- const result = formatResponse.formatFilesList(absolutePath, files, didHitLimit)
|
|
|
|
|
|
|
+ const result = formatResponse.formatFilesList(
|
|
|
|
|
+ absolutePath,
|
|
|
|
|
+ files,
|
|
|
|
|
+ didHitLimit,
|
|
|
|
|
+ this.rooIgnoreController,
|
|
|
|
|
+ )
|
|
|
const completeMessage = JSON.stringify({
|
|
const completeMessage = JSON.stringify({
|
|
|
...sharedMessageProps,
|
|
...sharedMessageProps,
|
|
|
content: result,
|
|
content: result,
|
|
@@ -2085,7 +2243,10 @@ export class Cline {
|
|
|
}
|
|
}
|
|
|
this.consecutiveMistakeCount = 0
|
|
this.consecutiveMistakeCount = 0
|
|
|
const absolutePath = path.resolve(cwd, relDirPath)
|
|
const absolutePath = path.resolve(cwd, relDirPath)
|
|
|
- const result = await parseSourceCodeForDefinitionsTopLevel(absolutePath)
|
|
|
|
|
|
|
+ const result = await parseSourceCodeForDefinitionsTopLevel(
|
|
|
|
|
+ absolutePath,
|
|
|
|
|
+ this.rooIgnoreController,
|
|
|
|
|
+ )
|
|
|
const completeMessage = JSON.stringify({
|
|
const completeMessage = JSON.stringify({
|
|
|
...sharedMessageProps,
|
|
...sharedMessageProps,
|
|
|
content: result,
|
|
content: result,
|
|
@@ -2133,7 +2294,13 @@ export class Cline {
|
|
|
}
|
|
}
|
|
|
this.consecutiveMistakeCount = 0
|
|
this.consecutiveMistakeCount = 0
|
|
|
const absolutePath = path.resolve(cwd, relDirPath)
|
|
const absolutePath = path.resolve(cwd, relDirPath)
|
|
|
- const results = await regexSearchFiles(cwd, absolutePath, regex, filePattern)
|
|
|
|
|
|
|
+ const results = await regexSearchFiles(
|
|
|
|
|
+ cwd,
|
|
|
|
|
+ absolutePath,
|
|
|
|
|
+ regex,
|
|
|
|
|
+ filePattern,
|
|
|
|
|
+ this.rooIgnoreController,
|
|
|
|
|
+ )
|
|
|
const completeMessage = JSON.stringify({
|
|
const completeMessage = JSON.stringify({
|
|
|
...sharedMessageProps,
|
|
...sharedMessageProps,
|
|
|
content: results,
|
|
content: results,
|
|
@@ -2312,6 +2479,19 @@ export class Cline {
|
|
|
)
|
|
)
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ const ignoredFileAttemptedToAccess = this.rooIgnoreController?.validateCommand(command)
|
|
|
|
|
+ if (ignoredFileAttemptedToAccess) {
|
|
|
|
|
+ await this.say("rooignore_error", ignoredFileAttemptedToAccess)
|
|
|
|
|
+ pushToolResult(
|
|
|
|
|
+ formatResponse.toolError(
|
|
|
|
|
+ formatResponse.rooIgnoreError(ignoredFileAttemptedToAccess),
|
|
|
|
|
+ ),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
this.consecutiveMistakeCount = 0
|
|
this.consecutiveMistakeCount = 0
|
|
|
|
|
|
|
|
const didApprove = await askApproval("command", command)
|
|
const didApprove = await askApproval("command", command)
|
|
@@ -2565,10 +2745,7 @@ export class Cline {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Switch the mode using shared handler
|
|
// Switch the mode using shared handler
|
|
|
- const provider = this.providerRef.deref()
|
|
|
|
|
- if (provider) {
|
|
|
|
|
- await provider.handleModeSwitch(mode_slug)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ await this.providerRef.deref()?.handleModeSwitch(mode_slug)
|
|
|
pushToolResult(
|
|
pushToolResult(
|
|
|
`Successfully switched from ${getModeBySlug(currentMode)?.name ?? currentMode} mode to ${
|
|
`Successfully switched from ${getModeBySlug(currentMode)?.name ?? currentMode} mode to ${
|
|
|
targetMode.name
|
|
targetMode.name
|
|
@@ -2630,19 +2807,25 @@ export class Cline {
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // before switching roo mode (currently a global settings), save the current mode so we can
|
|
|
|
|
+ // resume the parent task (this Cline instance) later with the same mode
|
|
|
|
|
+ const currentMode =
|
|
|
|
|
+ (await this.providerRef.deref()?.getState())?.mode ?? defaultModeSlug
|
|
|
|
|
+ this.pausedModeSlug = currentMode
|
|
|
|
|
+
|
|
|
// Switch mode first, then create new task instance
|
|
// Switch mode first, then create new task instance
|
|
|
- const provider = this.providerRef.deref()
|
|
|
|
|
- if (provider) {
|
|
|
|
|
- await provider.handleModeSwitch(mode)
|
|
|
|
|
- await provider.initClineWithTask(message)
|
|
|
|
|
- pushToolResult(
|
|
|
|
|
- `Successfully created new task in ${targetMode.name} mode with message: ${message}`,
|
|
|
|
|
- )
|
|
|
|
|
- } else {
|
|
|
|
|
- pushToolResult(
|
|
|
|
|
- formatResponse.toolError("Failed to create new task: provider not available"),
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ await this.providerRef.deref()?.handleModeSwitch(mode)
|
|
|
|
|
+ // wait for mode to actually switch in UI and in State
|
|
|
|
|
+ await delay(500) // delay to allow mode change to take effect before next tool is executed
|
|
|
|
|
+ this.providerRef
|
|
|
|
|
+ .deref()
|
|
|
|
|
+ ?.log(`[subtasks] Task: ${this.taskNumber} creating new task in '${mode}' mode`)
|
|
|
|
|
+ await this.providerRef.deref()?.initClineWithSubTask(message)
|
|
|
|
|
+ pushToolResult(
|
|
|
|
|
+ `Successfully created new task in ${targetMode.name} mode with message: ${message}`,
|
|
|
|
|
+ )
|
|
|
|
|
+ // set the isPaused flag to true so the parent task can wait for the sub-task to finish
|
|
|
|
|
+ this.isPaused = true
|
|
|
break
|
|
break
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
@@ -2698,6 +2881,15 @@ export class Cline {
|
|
|
undefined,
|
|
undefined,
|
|
|
false,
|
|
false,
|
|
|
)
|
|
)
|
|
|
|
|
+
|
|
|
|
|
+ if (this.isSubTask) {
|
|
|
|
|
+ // tell the provider to remove the current subtask and resume the previous task in the stack (it might decide to run the command)
|
|
|
|
|
+ await this.providerRef
|
|
|
|
|
+ .deref()
|
|
|
|
|
+ ?.finishSubTask(`new_task finished successfully! ${lastMessage?.text}`)
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
await this.ask(
|
|
await this.ask(
|
|
|
"command",
|
|
"command",
|
|
|
removeClosingTag("command", command),
|
|
removeClosingTag("command", command),
|
|
@@ -2729,6 +2921,13 @@ export class Cline {
|
|
|
if (lastMessage && lastMessage.ask !== "command") {
|
|
if (lastMessage && lastMessage.ask !== "command") {
|
|
|
// havent sent a command message yet so first send completion_result then command
|
|
// havent sent a command message yet so first send completion_result then command
|
|
|
await this.say("completion_result", result, undefined, false)
|
|
await this.say("completion_result", result, undefined, false)
|
|
|
|
|
+ if (this.isSubTask) {
|
|
|
|
|
+ // tell the provider to remove the current subtask and resume the previous task in the stack
|
|
|
|
|
+ await this.providerRef
|
|
|
|
|
+ .deref()
|
|
|
|
|
+ ?.finishSubTask(`Task complete: ${lastMessage?.text}`)
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// complete command message
|
|
// complete command message
|
|
@@ -2746,6 +2945,13 @@ export class Cline {
|
|
|
commandResult = execCommandResult
|
|
commandResult = execCommandResult
|
|
|
} else {
|
|
} else {
|
|
|
await this.say("completion_result", result, undefined, false)
|
|
await this.say("completion_result", result, undefined, false)
|
|
|
|
|
+ if (this.isSubTask) {
|
|
|
|
|
+ // tell the provider to remove the current subtask and resume the previous task in the stack
|
|
|
|
|
+ await this.providerRef
|
|
|
|
|
+ .deref()
|
|
|
|
|
+ ?.finishSubTask(`Task complete: ${lastMessage?.text}`)
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// we already sent completion_result says, an empty string asks relinquishes control over button and field
|
|
// we already sent completion_result says, an empty string asks relinquishes control over button and field
|
|
@@ -2821,12 +3027,26 @@ export class Cline {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // this function checks if this Cline instance is set to pause state and wait for being resumed,
|
|
|
|
|
+ // this is used when a sub-task is launched and the parent task is waiting for it to finish
|
|
|
|
|
+ async waitForResume() {
|
|
|
|
|
+ // wait until isPaused is false
|
|
|
|
|
+ await new Promise<void>((resolve) => {
|
|
|
|
|
+ const interval = setInterval(() => {
|
|
|
|
|
+ if (!this.isPaused) {
|
|
|
|
|
+ clearInterval(interval)
|
|
|
|
|
+ resolve()
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 1000) // TBD: the 1 sec should be added to the settings, also should add a timeout to prevent infinit wait
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
async recursivelyMakeClineRequests(
|
|
async recursivelyMakeClineRequests(
|
|
|
userContent: UserContent,
|
|
userContent: UserContent,
|
|
|
includeFileDetails: boolean = false,
|
|
includeFileDetails: boolean = false,
|
|
|
): Promise<boolean> {
|
|
): Promise<boolean> {
|
|
|
if (this.abort) {
|
|
if (this.abort) {
|
|
|
- throw new Error("Roo Code instance aborted")
|
|
|
|
|
|
|
+ throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#4)`)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (this.consecutiveMistakeCount >= 3) {
|
|
if (this.consecutiveMistakeCount >= 3) {
|
|
@@ -2853,6 +3073,27 @@ export class Cline {
|
|
|
// get previous api req's index to check token usage and determine if we need to truncate conversation history
|
|
// get previous api req's index to check token usage and determine if we need to truncate conversation history
|
|
|
const previousApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
|
|
const previousApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
|
|
|
|
|
|
|
|
|
|
+ // in this Cline request loop, we need to check if this cline (Task) instance has been asked to wait
|
|
|
|
|
+ // for a sub-task (it has launched) to finish before continuing
|
|
|
|
|
+ if (this.isPaused) {
|
|
|
|
|
+ this.providerRef.deref()?.log(`[subtasks] Task: ${this.taskNumber} has paused`)
|
|
|
|
|
+ await this.waitForResume()
|
|
|
|
|
+ this.providerRef.deref()?.log(`[subtasks] Task: ${this.taskNumber} has resumed`)
|
|
|
|
|
+ // waiting for resume is done, resume the task mode
|
|
|
|
|
+ const currentMode = (await this.providerRef.deref()?.getState())?.mode ?? defaultModeSlug
|
|
|
|
|
+ if (currentMode !== this.pausedModeSlug) {
|
|
|
|
|
+ // the mode has changed, we need to switch back to the paused mode
|
|
|
|
|
+ await this.providerRef.deref()?.handleModeSwitch(this.pausedModeSlug)
|
|
|
|
|
+ // wait for mode to actually switch in UI and in State
|
|
|
|
|
+ await delay(500) // delay to allow mode change to take effect before next tool is executed
|
|
|
|
|
+ this.providerRef
|
|
|
|
|
+ .deref()
|
|
|
|
|
+ ?.log(
|
|
|
|
|
+ `[subtasks] Task: ${this.taskNumber} has switched back to mode: '${this.pausedModeSlug}' from mode: '${currentMode}'`,
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds
|
|
// getting verbose details is an expensive operation, it uses globby to top-down build file structure of project which for large projects can take a few seconds
|
|
|
// for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
|
|
// for the best UX we show a placeholder api_req_started message with a loading spinner as this happens
|
|
|
await this.say(
|
|
await this.say(
|
|
@@ -3042,7 +3283,7 @@ export class Cline {
|
|
|
|
|
|
|
|
// need to call here in case the stream was aborted
|
|
// need to call here in case the stream was aborted
|
|
|
if (this.abort || this.abandoned) {
|
|
if (this.abort || this.abandoned) {
|
|
|
- throw new Error("Roo Code instance aborted")
|
|
|
|
|
|
|
+ throw new Error(`Task: ${this.taskNumber} Roo Code instance aborted (#5)`)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
this.didCompleteReadingStream = true
|
|
this.didCompleteReadingStream = true
|
|
@@ -3172,13 +3413,18 @@ export class Cline {
|
|
|
|
|
|
|
|
// It could be useful for cline to know if the user went from one or no file to another between messages, so we always include this context
|
|
// It could be useful for cline to know if the user went from one or no file to another between messages, so we always include this context
|
|
|
details += "\n\n# VSCode Visible Files"
|
|
details += "\n\n# VSCode Visible Files"
|
|
|
- const visibleFiles = vscode.window.visibleTextEditors
|
|
|
|
|
|
|
+ const visibleFilePaths = vscode.window.visibleTextEditors
|
|
|
?.map((editor) => editor.document?.uri?.fsPath)
|
|
?.map((editor) => editor.document?.uri?.fsPath)
|
|
|
.filter(Boolean)
|
|
.filter(Boolean)
|
|
|
- .map((absolutePath) => path.relative(cwd, absolutePath).toPosix())
|
|
|
|
|
- .join("\n")
|
|
|
|
|
- if (visibleFiles) {
|
|
|
|
|
- details += `\n${visibleFiles}`
|
|
|
|
|
|
|
+ .map((absolutePath) => path.relative(cwd, absolutePath))
|
|
|
|
|
+
|
|
|
|
|
+ // Filter paths through rooIgnoreController
|
|
|
|
|
+ const allowedVisibleFiles = this.rooIgnoreController
|
|
|
|
|
+ ? this.rooIgnoreController.filterPaths(visibleFilePaths)
|
|
|
|
|
+ : visibleFilePaths.map((p) => p.toPosix()).join("\n")
|
|
|
|
|
+
|
|
|
|
|
+ if (allowedVisibleFiles) {
|
|
|
|
|
+ details += `\n${allowedVisibleFiles}`
|
|
|
} else {
|
|
} else {
|
|
|
details += "\n(No visible files)"
|
|
details += "\n(No visible files)"
|
|
|
}
|
|
}
|
|
@@ -3186,15 +3432,20 @@ export class Cline {
|
|
|
details += "\n\n# VSCode Open Tabs"
|
|
details += "\n\n# VSCode Open Tabs"
|
|
|
const { maxOpenTabsContext } = (await this.providerRef.deref()?.getState()) ?? {}
|
|
const { maxOpenTabsContext } = (await this.providerRef.deref()?.getState()) ?? {}
|
|
|
const maxTabs = maxOpenTabsContext ?? 20
|
|
const maxTabs = maxOpenTabsContext ?? 20
|
|
|
- const openTabs = vscode.window.tabGroups.all
|
|
|
|
|
|
|
+ const openTabPaths = vscode.window.tabGroups.all
|
|
|
.flatMap((group) => group.tabs)
|
|
.flatMap((group) => group.tabs)
|
|
|
.map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
|
|
.map((tab) => (tab.input as vscode.TabInputText)?.uri?.fsPath)
|
|
|
.filter(Boolean)
|
|
.filter(Boolean)
|
|
|
.map((absolutePath) => path.relative(cwd, absolutePath).toPosix())
|
|
.map((absolutePath) => path.relative(cwd, absolutePath).toPosix())
|
|
|
.slice(0, maxTabs)
|
|
.slice(0, maxTabs)
|
|
|
- .join("\n")
|
|
|
|
|
- if (openTabs) {
|
|
|
|
|
- details += `\n${openTabs}`
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Filter paths through rooIgnoreController
|
|
|
|
|
+ const allowedOpenTabs = this.rooIgnoreController
|
|
|
|
|
+ ? this.rooIgnoreController.filterPaths(openTabPaths)
|
|
|
|
|
+ : openTabPaths.map((p) => p.toPosix()).join("\n")
|
|
|
|
|
+
|
|
|
|
|
+ if (allowedOpenTabs) {
|
|
|
|
|
+ details += `\n${allowedOpenTabs}`
|
|
|
} else {
|
|
} else {
|
|
|
details += "\n(No open tabs)"
|
|
details += "\n(No open tabs)"
|
|
|
}
|
|
}
|
|
@@ -3353,7 +3604,7 @@ export class Cline {
|
|
|
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
|
|
details += "(Desktop files not shown automatically. Use list_files to explore if needed.)"
|
|
|
} else {
|
|
} else {
|
|
|
const [files, didHitLimit] = await listFiles(cwd, true, 200)
|
|
const [files, didHitLimit] = await listFiles(cwd, true, 200)
|
|
|
- const result = formatResponse.formatFilesList(cwd, files, didHitLimit)
|
|
|
|
|
|
|
+ const result = formatResponse.formatFilesList(cwd, files, didHitLimit, this.rooIgnoreController)
|
|
|
details += result
|
|
details += result
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|