Browse Source

Merge pull request #1491 from shaybc/sbc_subtasks_approve_btn

Sbc subtasks approve button
Matt Rubens 10 months ago
parent
commit
e945dcf94f

+ 21 - 12
src/core/Cline.ts

@@ -1417,6 +1417,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(
@@ -2945,13 +2957,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
@@ -2970,13 +2975,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

+ 7 - 0
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,

+ 3 - 0
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

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -48,6 +48,7 @@ export interface WebviewMessage {
 		| "alwaysAllowBrowser"
 		| "alwaysAllowMcp"
 		| "alwaysAllowModeSwitch"
+		| "alwaysAllowFinishTask"
 		| "playSound"
 		| "soundEnabled"
 		| "soundVolume"

+ 1 - 0
src/shared/globalState.ts

@@ -40,6 +40,7 @@ export const GLOBAL_STATE_KEYS = [
 	"alwaysAllowBrowser",
 	"alwaysAllowMcp",
 	"alwaysAllowModeSwitch",
+	"alwaysAllowFinishTask",
 	"taskHistory",
 	"openAiBaseUrl",
 	"openAiModelId",

+ 16 - 0
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,
 	}
 

+ 12 - 0
webview-ui/src/components/chat/ChatRow.tsx

@@ -459,6 +459,18 @@ export const ChatRowContent = ({
 						</div>
 					</>
 				)
+			case "finishTask":
+				return (
+					<>
+						<div style={headerStyle}>
+							{toolIcon("new-file")}
+							<span style={{ fontWeight: "bold" }}>Roo wants to finish this task</span>
+						</div>
+						<div style={{ paddingLeft: "26px", marginTop: "4px" }}>
+							<code>{tool.content}</code>
+						</div>
+					</>
+				)
 			default:
 				return null
 		}

+ 10 - 1
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,
 		],
 	)
 

+ 14 - 0
webview-ui/src/components/settings/AutoApproveSettings.tsx

@@ -18,6 +18,7 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
 	requestDelaySeconds: number
 	alwaysAllowMcp?: boolean
 	alwaysAllowModeSwitch?: boolean
+	alwaysAllowFinishTask?: boolean
 	alwaysAllowExecute?: boolean
 	allowedCommands?: string[]
 	setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
@@ -32,6 +33,7 @@ export const AutoApproveSettings = ({
 	requestDelaySeconds,
 	alwaysAllowMcp,
 	alwaysAllowModeSwitch,
+	alwaysAllowFinishTask,
 	alwaysAllowExecute,
 	allowedCommands,
 	setCachedStateField,
@@ -180,6 +182,18 @@ export const AutoApproveSettings = ({
 					</p>
 				</div>
 
+				<div>
+					<VSCodeCheckbox
+						checked={alwaysAllowFinishTask}
+						onChange={(e: any) => setCachedStateField("alwaysAllowFinishTask", e.target.checked)}>
+						<span className="font-medium">Always approve finish & continue to next task</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Automatically approve tasks to finish execution and continue to the next task, without user
+						review or approval
+					</p>
+				</div>
+
 				<div>
 					<VSCodeCheckbox
 						checked={alwaysAllowExecute}

+ 3 - 0
webview-ui/src/components/settings/SettingsView.tsx

@@ -63,6 +63,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 		alwaysAllowExecute,
 		alwaysAllowMcp,
 		alwaysAllowModeSwitch,
+		alwaysAllowFinishTask,
 		alwaysAllowWrite,
 		alwaysApproveResubmit,
 		browserToolEnabled,
@@ -184,6 +185,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ 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<SettingsViewRef, SettingsViewProps>(({ onDone },
 						requestDelaySeconds={requestDelaySeconds}
 						alwaysAllowMcp={alwaysAllowMcp}
 						alwaysAllowModeSwitch={alwaysAllowModeSwitch}
+						alwaysAllowFinishTask={alwaysAllowFinishTask}
 						alwaysAllowExecute={alwaysAllowExecute}
 						allowedCommands={allowedCommands}
 						setCachedStateField={setCachedStateField}

+ 2 - 0
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 })),