mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Move new_task, switch_mode, attempt_completion, and ask_followup_question to tool files (#2102)
This commit is contained in:
parent
9b0790fce7
commit
253d0fecb2
6 changed files with 424 additions and 353 deletions
|
|
@ -95,6 +95,10 @@ 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 } from "./tools/attemptCompletionTool"
|
||||
import { newTaskTool } from "./tools/newTaskTool"
|
||||
|
||||
export type ToolResponse = string | Array<Anthropic.TextBlockParam | Anthropic.ImageBlockParam>
|
||||
type UserContent = Array<Anthropic.Messages.ContentBlockParam>
|
||||
|
|
@ -137,8 +141,8 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
readonly rootTask: Cline | undefined = undefined
|
||||
readonly parentTask: Cline | undefined = undefined
|
||||
readonly taskNumber: number
|
||||
private isPaused: boolean = false
|
||||
private pausedModeSlug: string = defaultModeSlug
|
||||
isPaused: boolean = false
|
||||
pausedModeSlug: string = defaultModeSlug
|
||||
private pauseInterval: NodeJS.Timeout | undefined
|
||||
|
||||
readonly apiConfiguration: ApiConfiguration
|
||||
|
|
@ -182,7 +186,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
private assistantMessageContent: AssistantMessageContent[] = []
|
||||
private presentAssistantMessageLocked = false
|
||||
private presentAssistantMessageHasPendingUpdates = false
|
||||
private userMessageContent: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
|
||||
userMessageContent: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
|
||||
private userMessageContentReady = false
|
||||
didRejectTool = false
|
||||
private didAlreadyUseTool = false
|
||||
|
|
@ -379,7 +383,7 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
this.emit("message", { action: "updated", message: partialMessage })
|
||||
}
|
||||
|
||||
private getTokenUsage() {
|
||||
getTokenUsage() {
|
||||
const usage = getApiMetrics(combineApiRequests(combineCommandSequences(this.clineMessages.slice(1))))
|
||||
this.emit("taskTokenUsageUpdated", this.taskId, usage)
|
||||
return usage
|
||||
|
|
@ -1647,363 +1651,38 @@ export class Cline extends EventEmitter<ClineEvents> {
|
|||
break
|
||||
}
|
||||
case "ask_followup_question": {
|
||||
const question: string | undefined = block.params.question
|
||||
const follow_up: string | undefined = block.params.follow_up
|
||||
try {
|
||||
if (block.partial) {
|
||||
await this.ask("followup", removeClosingTag("question", question), block.partial).catch(
|
||||
() => {},
|
||||
)
|
||||
break
|
||||
} else {
|
||||
if (!question) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(
|
||||
await this.sayAndCreateMissingParamError("ask_followup_question", "question"),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
type Suggest = {
|
||||
answer: string
|
||||
}
|
||||
|
||||
let follow_up_json = {
|
||||
question,
|
||||
suggest: [] as Suggest[],
|
||||
}
|
||||
|
||||
if (follow_up) {
|
||||
let parsedSuggest: {
|
||||
suggest: Suggest[] | Suggest
|
||||
}
|
||||
|
||||
try {
|
||||
parsedSuggest = parseXml(follow_up, ["suggest"]) as {
|
||||
suggest: Suggest[] | Suggest
|
||||
}
|
||||
} catch (error) {
|
||||
this.consecutiveMistakeCount++
|
||||
await this.say("error", `Failed to parse operations: ${error.message}`)
|
||||
pushToolResult(formatResponse.toolError("Invalid operations xml format"))
|
||||
break
|
||||
}
|
||||
|
||||
const normalizedSuggest = Array.isArray(parsedSuggest?.suggest)
|
||||
? parsedSuggest.suggest
|
||||
: [parsedSuggest?.suggest].filter((sug): sug is Suggest => sug !== undefined)
|
||||
|
||||
follow_up_json.suggest = normalizedSuggest
|
||||
}
|
||||
|
||||
this.consecutiveMistakeCount = 0
|
||||
|
||||
const { text, images } = await this.ask(
|
||||
"followup",
|
||||
JSON.stringify(follow_up_json),
|
||||
false,
|
||||
)
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
pushToolResult(formatResponse.toolResult(`<answer>\n${text}\n</answer>`, images))
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("asking question", error)
|
||||
break
|
||||
}
|
||||
await askFollowupQuestionTool(
|
||||
this,
|
||||
block,
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
)
|
||||
break
|
||||
}
|
||||
case "switch_mode": {
|
||||
const mode_slug: string | undefined = block.params.mode_slug
|
||||
const reason: string | undefined = block.params.reason
|
||||
try {
|
||||
if (block.partial) {
|
||||
const partialMessage = JSON.stringify({
|
||||
tool: "switchMode",
|
||||
mode: removeClosingTag("mode_slug", mode_slug),
|
||||
reason: removeClosingTag("reason", reason),
|
||||
})
|
||||
await this.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
break
|
||||
} else {
|
||||
if (!mode_slug) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("switch_mode", "mode_slug"))
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
|
||||
// Verify the mode exists
|
||||
const targetMode = getModeBySlug(
|
||||
mode_slug,
|
||||
(await this.providerRef.deref()?.getState())?.customModes,
|
||||
)
|
||||
if (!targetMode) {
|
||||
pushToolResult(formatResponse.toolError(`Invalid mode: ${mode_slug}`))
|
||||
break
|
||||
}
|
||||
|
||||
// Check if already in requested mode
|
||||
const currentMode =
|
||||
(await this.providerRef.deref()?.getState())?.mode ?? defaultModeSlug
|
||||
if (currentMode === mode_slug) {
|
||||
pushToolResult(`Already in ${targetMode.name} mode.`)
|
||||
break
|
||||
}
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
tool: "switchMode",
|
||||
mode: mode_slug,
|
||||
reason,
|
||||
})
|
||||
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
break
|
||||
}
|
||||
|
||||
// Switch the mode using shared handler
|
||||
await this.providerRef.deref()?.handleModeSwitch(mode_slug)
|
||||
pushToolResult(
|
||||
`Successfully switched from ${getModeBySlug(currentMode)?.name ?? currentMode} mode to ${
|
||||
targetMode.name
|
||||
} mode${reason ? ` because: ${reason}` : ""}.`,
|
||||
)
|
||||
await delay(500) // delay to allow mode change to take effect before next tool is executed
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("switching mode", error)
|
||||
break
|
||||
}
|
||||
await switchModeTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
break
|
||||
}
|
||||
|
||||
case "new_task": {
|
||||
const mode: string | undefined = block.params.mode
|
||||
const message: string | undefined = block.params.message
|
||||
try {
|
||||
if (block.partial) {
|
||||
const partialMessage = JSON.stringify({
|
||||
tool: "newTask",
|
||||
mode: removeClosingTag("mode", mode),
|
||||
message: removeClosingTag("message", message),
|
||||
})
|
||||
await this.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
break
|
||||
} else {
|
||||
if (!mode) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("new_task", "mode"))
|
||||
break
|
||||
}
|
||||
if (!message) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(await this.sayAndCreateMissingParamError("new_task", "message"))
|
||||
break
|
||||
}
|
||||
this.consecutiveMistakeCount = 0
|
||||
|
||||
// Verify the mode exists
|
||||
const targetMode = getModeBySlug(
|
||||
mode,
|
||||
(await this.providerRef.deref()?.getState())?.customModes,
|
||||
)
|
||||
if (!targetMode) {
|
||||
pushToolResult(formatResponse.toolError(`Invalid mode: ${mode}`))
|
||||
break
|
||||
}
|
||||
|
||||
const toolMessage = JSON.stringify({
|
||||
tool: "newTask",
|
||||
mode: targetMode.name,
|
||||
content: message,
|
||||
})
|
||||
const didApprove = await askApproval("tool", toolMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
break
|
||||
}
|
||||
|
||||
const provider = this.providerRef.deref()
|
||||
|
||||
if (!provider) {
|
||||
break
|
||||
}
|
||||
|
||||
// Preserve the current mode so we can resume with it later.
|
||||
this.pausedModeSlug = (await provider.getState()).mode ?? defaultModeSlug
|
||||
|
||||
// Switch mode first, then create new task instance.
|
||||
await provider.handleModeSwitch(mode)
|
||||
|
||||
// Delay to allow mode change to take effect before next tool is executed.
|
||||
await delay(500)
|
||||
|
||||
const newCline = await provider.initClineWithTask(message, undefined, this)
|
||||
this.emit("taskSpawned", newCline.taskId)
|
||||
|
||||
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
|
||||
this.emit("taskPaused")
|
||||
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("creating new task", error)
|
||||
break
|
||||
}
|
||||
await newTaskTool(this, block, askApproval, handleError, pushToolResult, removeClosingTag)
|
||||
break
|
||||
}
|
||||
|
||||
case "attempt_completion": {
|
||||
const result: string | undefined = block.params.result
|
||||
const command: string | undefined = block.params.command
|
||||
try {
|
||||
const lastMessage = this.clineMessages.at(-1)
|
||||
if (block.partial) {
|
||||
if (command) {
|
||||
// the attempt_completion text is done, now we're getting command
|
||||
// remove the previous partial attempt_completion ask, replace with say, post state to webview, then stream command
|
||||
|
||||
// const secondLastMessage = this.clineMessages.at(-2)
|
||||
if (lastMessage && lastMessage.ask === "command") {
|
||||
// update command
|
||||
await this.ask(
|
||||
"command",
|
||||
removeClosingTag("command", command),
|
||||
block.partial,
|
||||
).catch(() => {})
|
||||
} else {
|
||||
// last message is completion_result
|
||||
// we have command string, which means we have the result as well, so finish it (doesnt have to exist yet)
|
||||
await this.say(
|
||||
"completion_result",
|
||||
removeClosingTag("result", result),
|
||||
undefined,
|
||||
false,
|
||||
)
|
||||
|
||||
telemetryService.captureTaskCompleted(this.taskId)
|
||||
this.emit("taskCompleted", this.taskId, this.getTokenUsage())
|
||||
|
||||
await this.ask(
|
||||
"command",
|
||||
removeClosingTag("command", command),
|
||||
block.partial,
|
||||
).catch(() => {})
|
||||
}
|
||||
} else {
|
||||
// no command, still outputting partial result
|
||||
await this.say(
|
||||
"completion_result",
|
||||
removeClosingTag("result", result),
|
||||
undefined,
|
||||
block.partial,
|
||||
)
|
||||
}
|
||||
break
|
||||
} else {
|
||||
if (!result) {
|
||||
this.consecutiveMistakeCount++
|
||||
pushToolResult(
|
||||
await this.sayAndCreateMissingParamError("attempt_completion", "result"),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
this.consecutiveMistakeCount = 0
|
||||
|
||||
let commandResult: ToolResponse | undefined
|
||||
|
||||
if (command) {
|
||||
if (lastMessage && lastMessage.ask !== "command") {
|
||||
// Haven't sent a command message yet so first send completion_result then command.
|
||||
await this.say("completion_result", result, undefined, false)
|
||||
telemetryService.captureTaskCompleted(this.taskId)
|
||||
this.emit("taskCompleted", this.taskId, this.getTokenUsage())
|
||||
}
|
||||
|
||||
// Complete command message.
|
||||
const didApprove = await askApproval("command", command)
|
||||
|
||||
if (!didApprove) {
|
||||
break
|
||||
}
|
||||
|
||||
const [userRejected, execCommandResult] = await this.executeCommandTool(command!)
|
||||
|
||||
if (userRejected) {
|
||||
this.didRejectTool = true
|
||||
pushToolResult(execCommandResult)
|
||||
break
|
||||
}
|
||||
|
||||
// User didn't reject, but the command may have output.
|
||||
commandResult = execCommandResult
|
||||
} else {
|
||||
await this.say("completion_result", result, undefined, false)
|
||||
telemetryService.captureTaskCompleted(this.taskId)
|
||||
this.emit("taskCompleted", this.taskId, this.getTokenUsage())
|
||||
}
|
||||
|
||||
if (this.parentTask) {
|
||||
const didApprove = await askFinishSubTaskApproval()
|
||||
|
||||
if (!didApprove) {
|
||||
break
|
||||
}
|
||||
|
||||
// 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.
|
||||
const { response, text, images } = await this.ask("completion_result", "", false)
|
||||
|
||||
// Signals to recursive loop to stop (for now
|
||||
// this never happens since yesButtonClicked
|
||||
// will trigger a new task).
|
||||
if (response === "yesButtonClicked") {
|
||||
pushToolResult("")
|
||||
break
|
||||
}
|
||||
|
||||
await this.say("user_feedback", text ?? "", images)
|
||||
const toolResults: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
|
||||
|
||||
if (commandResult) {
|
||||
if (typeof commandResult === "string") {
|
||||
toolResults.push({ type: "text", text: commandResult })
|
||||
} else if (Array.isArray(commandResult)) {
|
||||
toolResults.push(...commandResult)
|
||||
}
|
||||
}
|
||||
|
||||
toolResults.push({
|
||||
type: "text",
|
||||
text: `The user has provided feedback on the results. Consider their input to continue the task, and then attempt completion again.\n<feedback>\n${text}\n</feedback>`,
|
||||
})
|
||||
|
||||
toolResults.push(...formatResponse.imageBlocks(images))
|
||||
|
||||
this.userMessageContent.push({
|
||||
type: "text",
|
||||
text: `${toolDescription()} Result:`,
|
||||
})
|
||||
|
||||
this.userMessageContent.push(...toolResults)
|
||||
break
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("inspecting site", error)
|
||||
break
|
||||
}
|
||||
await attemptCompletionTool(
|
||||
this,
|
||||
block,
|
||||
askApproval,
|
||||
handleError,
|
||||
pushToolResult,
|
||||
removeClosingTag,
|
||||
toolDescription,
|
||||
askFinishSubTaskApproval,
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
71
src/core/tools/askFollowupQuestionTool.ts
Normal file
71
src/core/tools/askFollowupQuestionTool.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { Cline } from "../Cline"
|
||||
import { ToolUse } from "../assistant-message"
|
||||
import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "./types"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { parseXml } from "../../utils/xml"
|
||||
|
||||
export async function askFollowupQuestionTool(
|
||||
cline: Cline,
|
||||
block: ToolUse,
|
||||
askApproval: AskApproval,
|
||||
handleError: HandleError,
|
||||
pushToolResult: PushToolResult,
|
||||
removeClosingTag: RemoveClosingTag,
|
||||
) {
|
||||
const question: string | undefined = block.params.question
|
||||
const follow_up: string | undefined = block.params.follow_up
|
||||
try {
|
||||
if (block.partial) {
|
||||
await cline.ask("followup", removeClosingTag("question", question), block.partial).catch(() => {})
|
||||
return
|
||||
} else {
|
||||
if (!question) {
|
||||
cline.consecutiveMistakeCount++
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("ask_followup_question", "question"))
|
||||
return
|
||||
}
|
||||
|
||||
type Suggest = {
|
||||
answer: string
|
||||
}
|
||||
|
||||
let follow_up_json = {
|
||||
question,
|
||||
suggest: [] as Suggest[],
|
||||
}
|
||||
|
||||
if (follow_up) {
|
||||
let parsedSuggest: {
|
||||
suggest: Suggest[] | Suggest
|
||||
}
|
||||
|
||||
try {
|
||||
parsedSuggest = parseXml(follow_up, ["suggest"]) as {
|
||||
suggest: Suggest[] | Suggest
|
||||
}
|
||||
} catch (error) {
|
||||
cline.consecutiveMistakeCount++
|
||||
await cline.say("error", `Failed to parse operations: ${error.message}`)
|
||||
pushToolResult(formatResponse.toolError("Invalid operations xml format"))
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedSuggest = Array.isArray(parsedSuggest?.suggest)
|
||||
? parsedSuggest.suggest
|
||||
: [parsedSuggest?.suggest].filter((sug): sug is Suggest => sug !== undefined)
|
||||
|
||||
follow_up_json.suggest = normalizedSuggest
|
||||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
const { text, images } = await cline.ask("followup", JSON.stringify(follow_up_json), false)
|
||||
await cline.say("user_feedback", text ?? "", images)
|
||||
pushToolResult(formatResponse.toolResult(`<answer>\n${text}\n</answer>`, images))
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("asking question", error)
|
||||
return
|
||||
}
|
||||
}
|
||||
152
src/core/tools/attemptCompletionTool.ts
Normal file
152
src/core/tools/attemptCompletionTool.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import { ToolResponse } from "../Cline"
|
||||
|
||||
import { ToolUse } from "../assistant-message"
|
||||
import { Cline } from "../Cline"
|
||||
import {
|
||||
AskApproval,
|
||||
HandleError,
|
||||
PushToolResult,
|
||||
RemoveClosingTag,
|
||||
ToolDescription,
|
||||
AskFinishSubTaskApproval,
|
||||
} from "./types"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { telemetryService } from "../../services/telemetry/TelemetryService"
|
||||
import Anthropic from "@anthropic-ai/sdk"
|
||||
|
||||
export async function attemptCompletionTool(
|
||||
cline: Cline,
|
||||
block: ToolUse,
|
||||
askApproval: AskApproval,
|
||||
handleError: HandleError,
|
||||
pushToolResult: PushToolResult,
|
||||
removeClosingTag: RemoveClosingTag,
|
||||
toolDescription: ToolDescription,
|
||||
askFinishSubTaskApproval: AskFinishSubTaskApproval,
|
||||
) {
|
||||
const result: string | undefined = block.params.result
|
||||
const command: string | undefined = block.params.command
|
||||
try {
|
||||
const lastMessage = cline.clineMessages.at(-1)
|
||||
if (block.partial) {
|
||||
if (command) {
|
||||
// the attempt_completion text is done, now we're getting command
|
||||
// remove the previous partial attempt_completion ask, replace with say, post state to webview, then stream command
|
||||
|
||||
// const secondLastMessage = cline.clineMessages.at(-2)
|
||||
if (lastMessage && lastMessage.ask === "command") {
|
||||
// update command
|
||||
await cline.ask("command", removeClosingTag("command", command), block.partial).catch(() => {})
|
||||
} else {
|
||||
// last message is completion_result
|
||||
// we have command string, which means we have the result as well, so finish it (doesnt have to exist yet)
|
||||
await cline.say("completion_result", removeClosingTag("result", result), undefined, false)
|
||||
|
||||
telemetryService.captureTaskCompleted(cline.taskId)
|
||||
cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage())
|
||||
|
||||
await cline.ask("command", removeClosingTag("command", command), block.partial).catch(() => {})
|
||||
}
|
||||
} else {
|
||||
// no command, still outputting partial result
|
||||
await cline.say("completion_result", removeClosingTag("result", result), undefined, block.partial)
|
||||
}
|
||||
return
|
||||
} else {
|
||||
if (!result) {
|
||||
cline.consecutiveMistakeCount++
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("attempt_completion", "result"))
|
||||
return
|
||||
}
|
||||
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
let commandResult: ToolResponse | undefined
|
||||
|
||||
if (command) {
|
||||
if (lastMessage && lastMessage.ask !== "command") {
|
||||
// Haven't sent a command message yet so first send completion_result then command.
|
||||
await cline.say("completion_result", result, undefined, false)
|
||||
telemetryService.captureTaskCompleted(cline.taskId)
|
||||
cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage())
|
||||
}
|
||||
|
||||
// Complete command message.
|
||||
const didApprove = await askApproval("command", command)
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
const [userRejected, execCommandResult] = await cline.executeCommandTool(command!)
|
||||
|
||||
if (userRejected) {
|
||||
cline.didRejectTool = true
|
||||
pushToolResult(execCommandResult)
|
||||
return
|
||||
}
|
||||
|
||||
// User didn't reject, but the command may have output.
|
||||
commandResult = execCommandResult
|
||||
} else {
|
||||
await cline.say("completion_result", result, undefined, false)
|
||||
telemetryService.captureTaskCompleted(cline.taskId)
|
||||
cline.emit("taskCompleted", cline.taskId, cline.getTokenUsage())
|
||||
}
|
||||
|
||||
if (cline.parentTask) {
|
||||
const didApprove = await askFinishSubTaskApproval()
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
// tell the provider to remove the current subtask and resume the previous task in the stack
|
||||
await cline.providerRef.deref()?.finishSubTask(`Task complete: ${lastMessage?.text}`)
|
||||
return
|
||||
}
|
||||
|
||||
// We already sent completion_result says, an
|
||||
// empty string asks relinquishes control over
|
||||
// button and field.
|
||||
const { response, text, images } = await cline.ask("completion_result", "", false)
|
||||
|
||||
// Signals to recursive loop to stop (for now
|
||||
// cline never happens since yesButtonClicked
|
||||
// will trigger a new task).
|
||||
if (response === "yesButtonClicked") {
|
||||
pushToolResult("")
|
||||
return
|
||||
}
|
||||
|
||||
await cline.say("user_feedback", text ?? "", images)
|
||||
const toolResults: (Anthropic.TextBlockParam | Anthropic.ImageBlockParam)[] = []
|
||||
|
||||
if (commandResult) {
|
||||
if (typeof commandResult === "string") {
|
||||
toolResults.push({ type: "text", text: commandResult })
|
||||
} else if (Array.isArray(commandResult)) {
|
||||
toolResults.push(...commandResult)
|
||||
}
|
||||
}
|
||||
|
||||
toolResults.push({
|
||||
type: "text",
|
||||
text: `The user has provided feedback on the results. Consider their input to continue the task, and then attempt completion again.\n<feedback>\n${text}\n</feedback>`,
|
||||
})
|
||||
|
||||
toolResults.push(...formatResponse.imageBlocks(images))
|
||||
|
||||
cline.userMessageContent.push({
|
||||
type: "text",
|
||||
text: `${toolDescription()} Result:`,
|
||||
})
|
||||
|
||||
cline.userMessageContent.push(...toolResults)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("inspecting site", error)
|
||||
return
|
||||
}
|
||||
}
|
||||
90
src/core/tools/newTaskTool.ts
Normal file
90
src/core/tools/newTaskTool.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
import { ToolUse } from "../assistant-message"
|
||||
import { HandleError, PushToolResult, RemoveClosingTag } from "./types"
|
||||
import { Cline } from "../Cline"
|
||||
import { AskApproval } from "./types"
|
||||
import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import delay from "delay"
|
||||
|
||||
export async function newTaskTool(
|
||||
cline: Cline,
|
||||
block: ToolUse,
|
||||
askApproval: AskApproval,
|
||||
handleError: HandleError,
|
||||
pushToolResult: PushToolResult,
|
||||
removeClosingTag: RemoveClosingTag,
|
||||
) {
|
||||
const mode: string | undefined = block.params.mode
|
||||
const message: string | undefined = block.params.message
|
||||
try {
|
||||
if (block.partial) {
|
||||
const partialMessage = JSON.stringify({
|
||||
tool: "newTask",
|
||||
mode: removeClosingTag("mode", mode),
|
||||
message: removeClosingTag("message", message),
|
||||
})
|
||||
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
return
|
||||
} else {
|
||||
if (!mode) {
|
||||
cline.consecutiveMistakeCount++
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("new_task", "mode"))
|
||||
return
|
||||
}
|
||||
if (!message) {
|
||||
cline.consecutiveMistakeCount++
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("new_task", "message"))
|
||||
return
|
||||
}
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
// Verify the mode exists
|
||||
const targetMode = getModeBySlug(mode, (await cline.providerRef.deref()?.getState())?.customModes)
|
||||
if (!targetMode) {
|
||||
pushToolResult(formatResponse.toolError(`Invalid mode: ${mode}`))
|
||||
return
|
||||
}
|
||||
|
||||
const toolMessage = JSON.stringify({
|
||||
tool: "newTask",
|
||||
mode: targetMode.name,
|
||||
content: message,
|
||||
})
|
||||
const didApprove = await askApproval("tool", toolMessage)
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
const provider = cline.providerRef.deref()
|
||||
|
||||
if (!provider) {
|
||||
return
|
||||
}
|
||||
|
||||
// Preserve the current mode so we can resume with it later.
|
||||
cline.pausedModeSlug = (await provider.getState()).mode ?? defaultModeSlug
|
||||
|
||||
// Switch mode first, then create new task instance.
|
||||
await provider.handleModeSwitch(mode)
|
||||
|
||||
// Delay to allow mode change to take effect before next tool is executed.
|
||||
await delay(500)
|
||||
|
||||
const newCline = await provider.initClineWithTask(message, undefined, cline)
|
||||
cline.emit("taskSpawned", newCline.taskId)
|
||||
|
||||
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.
|
||||
cline.isPaused = true
|
||||
cline.emit("taskPaused")
|
||||
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("creating new task", error)
|
||||
return
|
||||
}
|
||||
}
|
||||
75
src/core/tools/switchModeTool.ts
Normal file
75
src/core/tools/switchModeTool.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { Cline } from "../Cline"
|
||||
import { ToolUse } from "../assistant-message"
|
||||
import { AskApproval, HandleError, PushToolResult, RemoveClosingTag } from "./types"
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
import { defaultModeSlug } from "../../shared/modes"
|
||||
import { getModeBySlug } from "../../shared/modes"
|
||||
import delay from "delay"
|
||||
|
||||
export async function switchModeTool(
|
||||
cline: Cline,
|
||||
block: ToolUse,
|
||||
askApproval: AskApproval,
|
||||
handleError: HandleError,
|
||||
pushToolResult: PushToolResult,
|
||||
removeClosingTag: RemoveClosingTag,
|
||||
) {
|
||||
const mode_slug: string | undefined = block.params.mode_slug
|
||||
const reason: string | undefined = block.params.reason
|
||||
try {
|
||||
if (block.partial) {
|
||||
const partialMessage = JSON.stringify({
|
||||
tool: "switchMode",
|
||||
mode: removeClosingTag("mode_slug", mode_slug),
|
||||
reason: removeClosingTag("reason", reason),
|
||||
})
|
||||
await cline.ask("tool", partialMessage, block.partial).catch(() => {})
|
||||
return
|
||||
} else {
|
||||
if (!mode_slug) {
|
||||
cline.consecutiveMistakeCount++
|
||||
pushToolResult(await cline.sayAndCreateMissingParamError("switch_mode", "mode_slug"))
|
||||
return
|
||||
}
|
||||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
// Verify the mode exists
|
||||
const targetMode = getModeBySlug(mode_slug, (await cline.providerRef.deref()?.getState())?.customModes)
|
||||
if (!targetMode) {
|
||||
pushToolResult(formatResponse.toolError(`Invalid mode: ${mode_slug}`))
|
||||
return
|
||||
}
|
||||
|
||||
// Check if already in requested mode
|
||||
const currentMode = (await cline.providerRef.deref()?.getState())?.mode ?? defaultModeSlug
|
||||
if (currentMode === mode_slug) {
|
||||
pushToolResult(`Already in ${targetMode.name} mode.`)
|
||||
return
|
||||
}
|
||||
|
||||
const completeMessage = JSON.stringify({
|
||||
tool: "switchMode",
|
||||
mode: mode_slug,
|
||||
reason,
|
||||
})
|
||||
|
||||
const didApprove = await askApproval("tool", completeMessage)
|
||||
if (!didApprove) {
|
||||
return
|
||||
}
|
||||
|
||||
// Switch the mode using shared handler
|
||||
await cline.providerRef.deref()?.handleModeSwitch(mode_slug)
|
||||
pushToolResult(
|
||||
`Successfully switched from ${getModeBySlug(currentMode)?.name ?? currentMode} mode to ${
|
||||
targetMode.name
|
||||
} mode${reason ? ` because: ${reason}` : ""}.`,
|
||||
)
|
||||
await delay(500) // delay to allow mode change to take effect before next tool is executed
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
await handleError("switching mode", error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -13,3 +13,7 @@ export type HandleError = (action: string, error: Error) => Promise<void>
|
|||
export type PushToolResult = (content: ToolResponse) => void
|
||||
|
||||
export type RemoveClosingTag = (tag: ToolParamName, content?: string) => string
|
||||
|
||||
export type AskFinishSubTaskApproval = () => Promise<boolean>
|
||||
|
||||
export type ToolDescription = () => string
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue