diff --git a/src/core/Cline.ts b/src/core/Cline.ts index 51fb5265d41..cd1ce7031d0 100644 --- a/src/core/Cline.ts +++ b/src/core/Cline.ts @@ -1414,6 +1414,18 @@ export class Cline { return true } + const askFinishSubTaskApproval = async () => { + // ask the user to approve this task has completed, and he has reviewd 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", + content: + "Task completed! You can review the results and suggest any corrections or next steps. If everything looks good, confirm to continue with the next task.", + }) + + return await askApproval("tool", toolMessage) + } + const handleError = async (action: string, error: Error) => { const errorString = `Error ${action}: ${JSON.stringify(serializeError(error))}` await this.say( @@ -2942,13 +2954,6 @@ export class Cline { // havent sent a command message yet so first send completion_result then command await this.say("completion_result", result, undefined, false) telemetryService.captureTaskCompleted(this.taskId) - 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 @@ -2967,13 +2972,17 @@ export class Cline { } else { await this.say("completion_result", result, undefined, false) telemetryService.captureTaskCompleted(this.taskId) - 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}`) + } + + if (this.isSubTask) { + 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 diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 70feb45c2fc..cc0b6f4f04a 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -984,6 +984,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("alwaysAllowModeSwitch", message.bool) await this.postStateToWebview() break + case "alwaysAllowFinishTask": + await this.updateGlobalState("alwaysAllowFinishTask", message.bool) + await this.postStateToWebview() + break case "askResponse": this.getCurrentCline()?.handleWebviewAskResponse( message.askResponse!, @@ -2177,6 +2181,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowBrowser, alwaysAllowMcp, alwaysAllowModeSwitch, + alwaysAllowFinishTask, soundEnabled, diffEnabled, enableCheckpoints, @@ -2224,6 +2229,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowBrowser: alwaysAllowBrowser ?? false, alwaysAllowMcp: alwaysAllowMcp ?? false, alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false, + alwaysAllowFinishTask: alwaysAllowFinishTask ?? false, uriScheme: vscode.env.uriScheme, currentTaskItem: this.getCurrentCline()?.taskId ? (taskHistory || []).find((item: HistoryItem) => item.id === this.getCurrentCline()?.taskId) @@ -2385,6 +2391,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { alwaysAllowBrowser: stateValues.alwaysAllowBrowser ?? false, alwaysAllowMcp: stateValues.alwaysAllowMcp ?? false, alwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false, + alwaysAllowFinishTask: stateValues.alwaysAllowFinishTask ?? false, taskHistory: stateValues.taskHistory, allowedCommands: stateValues.allowedCommands, soundEnabled: stateValues.soundEnabled ?? false, diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 98ff9b36e15..dcdaf017f36 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -109,6 +109,7 @@ export interface ExtensionState { alwaysAllowMcp?: boolean alwaysApproveResubmit?: boolean alwaysAllowModeSwitch?: boolean + alwaysAllowFinishTask?: boolean browserToolEnabled?: boolean requestDelaySeconds: number rateLimitSeconds: number // Minimum time between successive requests (0 = disabled) @@ -168,6 +169,7 @@ export type ClineAsk = | "mistake_limit_reached" | "browser_action_launch" | "use_mcp_server" + | "finishTask" export type ClineSay = | "task" @@ -207,6 +209,7 @@ export interface ClineSayTool { | "searchFiles" | "switchMode" | "newTask" + | "finishTask" path?: string diff?: string content?: string diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 10af6f7a946..086701a43fd 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -48,6 +48,7 @@ export interface WebviewMessage { | "alwaysAllowBrowser" | "alwaysAllowMcp" | "alwaysAllowModeSwitch" + | "alwaysAllowFinishTask" | "playSound" | "soundEnabled" | "soundVolume" diff --git a/src/shared/globalState.ts b/src/shared/globalState.ts index bfd24f42984..739fa11dad7 100644 --- a/src/shared/globalState.ts +++ b/src/shared/globalState.ts @@ -40,6 +40,7 @@ export const GLOBAL_STATE_KEYS = [ "alwaysAllowBrowser", "alwaysAllowMcp", "alwaysAllowModeSwitch", + "alwaysAllowFinishTask", "taskHistory", "openAiBaseUrl", "openAiModelId", diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx index 161f3032b07..fba97f6c7d5 100644 --- a/webview-ui/src/components/chat/AutoApproveMenu.tsx +++ b/webview-ui/src/components/chat/AutoApproveMenu.tsx @@ -30,6 +30,8 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { setAlwaysAllowMcp, alwaysAllowModeSwitch, setAlwaysAllowModeSwitch, + alwaysAllowFinishTask, + setAlwaysAllowFinishTask, alwaysApproveResubmit, setAlwaysApproveResubmit, autoApprovalEnabled, @@ -81,6 +83,13 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { description: "Allows automatic switching between different AI modes and creating new tasks without requiring approval.", }, + { + id: "finishTask", + label: "Continue to next task", + shortName: "Continue", + enabled: alwaysAllowFinishTask ?? false, + description: "Allow tasks to end execution and continue to the next task, without user review or approval.", + }, { id: "retryRequests", label: "Retry failed requests", @@ -136,6 +145,12 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: newValue }) }, [alwaysAllowModeSwitch, setAlwaysAllowModeSwitch]) + const handleFinishTaskChange = useCallback(() => { + const newValue = !(alwaysAllowFinishTask ?? false) + setAlwaysAllowFinishTask(newValue) + vscode.postMessage({ type: "alwaysAllowFinishTask", bool: newValue }) + }, [alwaysAllowFinishTask, setAlwaysAllowFinishTask]) + const handleRetryChange = useCallback(() => { const newValue = !(alwaysApproveResubmit ?? false) setAlwaysApproveResubmit(newValue) @@ -150,6 +165,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { useBrowser: handleBrowserChange, useMcp: handleMcpChange, switchModes: handleModeSwitchChange, + finishTask: handleFinishTaskChange, retryRequests: handleRetryChange, } diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 1533bba3a8f..6f19df665e2 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -459,6 +459,18 @@ export const ChatRowContent = ({ ) + case "finishTask": + return ( + <> +
+ {toolIcon("new-file")} + Roo wants to finish this task +
+
+ {tool.content} +
+ + ) default: return null } diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index 35e63dd3329..b92604e157d 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -61,6 +61,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie setMode, autoApprovalEnabled, alwaysAllowModeSwitch, + alwaysAllowFinishTask, customModes, telemetrySetting, } = useExtensionState() @@ -148,6 +149,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie setPrimaryButtonText("Save") setSecondaryButtonText("Reject") break + case "finishTask": + setPrimaryButtonText("Approve & Continue to the next Task") + setSecondaryButtonText(undefined) + break default: setPrimaryButtonText("Approve") setSecondaryButtonText("Reject") @@ -642,7 +647,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie (alwaysAllowModeSwitch && message.ask === "tool" && (JSON.parse(message.text || "{}")?.tool === "switchMode" || - JSON.parse(message.text || "{}")?.tool === "newTask")) + JSON.parse(message.text || "{}")?.tool === "newTask")) || + (alwaysAllowFinishTask && + message.ask === "tool" && + JSON.parse(message.text || "{}")?.tool === "finishTask") ) }, [ @@ -657,6 +665,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie alwaysAllowMcp, isMcpToolAlwaysAllowed, alwaysAllowModeSwitch, + alwaysAllowFinishTask, ], ) diff --git a/webview-ui/src/components/settings/AutoApproveSettings.tsx b/webview-ui/src/components/settings/AutoApproveSettings.tsx index b2da2cab759..1c8e6c9ea91 100644 --- a/webview-ui/src/components/settings/AutoApproveSettings.tsx +++ b/webview-ui/src/components/settings/AutoApproveSettings.tsx @@ -18,6 +18,7 @@ type AutoApproveSettingsProps = HTMLAttributes & { requestDelaySeconds: number alwaysAllowMcp?: boolean alwaysAllowModeSwitch?: boolean + alwaysAllowFinishTask?: boolean alwaysAllowExecute?: boolean allowedCommands?: string[] setCachedStateField: SetCachedStateField @@ -32,6 +33,7 @@ export const AutoApproveSettings = ({ requestDelaySeconds, alwaysAllowMcp, alwaysAllowModeSwitch, + alwaysAllowFinishTask, alwaysAllowExecute, allowedCommands, setCachedStateField, @@ -180,6 +182,18 @@ export const AutoApproveSettings = ({

+
+ setCachedStateField("alwaysAllowFinishTask", e.target.checked)}> + Always approve finish & continue to next task + +

+ Automatically approve tasks to finish execution and continue to the next task, without user + review or approval +

+
+
(({ onDone }, alwaysAllowExecute, alwaysAllowMcp, alwaysAllowModeSwitch, + alwaysAllowFinishTask, alwaysAllowWrite, alwaysApproveResubmit, browserToolEnabled, @@ -184,6 +185,7 @@ const SettingsView = forwardRef(({ onDone }, vscode.postMessage({ type: "currentApiConfigName", text: currentApiConfigName }) vscode.postMessage({ type: "updateExperimental", values: experiments }) vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch }) + vscode.postMessage({ type: "alwaysAllowFinishTask", bool: alwaysAllowFinishTask }) vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting }) setChangeDetected(false) @@ -364,6 +366,7 @@ const SettingsView = forwardRef(({ onDone }, requestDelaySeconds={requestDelaySeconds} alwaysAllowMcp={alwaysAllowMcp} alwaysAllowModeSwitch={alwaysAllowModeSwitch} + alwaysAllowFinishTask={alwaysAllowFinishTask} alwaysAllowExecute={alwaysAllowExecute} allowedCommands={allowedCommands} setCachedStateField={setCachedStateField} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index c4daf426ca1..aa132919c02 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -31,6 +31,7 @@ export interface ExtensionStateContextType extends ExtensionState { setAlwaysAllowBrowser: (value: boolean) => void setAlwaysAllowMcp: (value: boolean) => void setAlwaysAllowModeSwitch: (value: boolean) => void + setAlwaysAllowFinishTask: (value: boolean) => void setBrowserToolEnabled: (value: boolean) => void setShowRooIgnoredFiles: (value: boolean) => void setShowAnnouncement: (value: boolean) => void @@ -247,6 +248,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setAlwaysAllowBrowser: (value) => setState((prevState) => ({ ...prevState, alwaysAllowBrowser: value })), setAlwaysAllowMcp: (value) => setState((prevState) => ({ ...prevState, alwaysAllowMcp: value })), setAlwaysAllowModeSwitch: (value) => setState((prevState) => ({ ...prevState, alwaysAllowModeSwitch: value })), + setAlwaysAllowFinishTask: (value) => setState((prevState) => ({ ...prevState, alwaysAllowFinishTask: value })), setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })), setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })), setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),