Skip to content

Commit 878a8a2

Browse files
committed
More fixes
1 parent b4c3fb4 commit 878a8a2

File tree

4 files changed

+116
-82
lines changed

4 files changed

+116
-82
lines changed

src/core/auto-approval/index.ts

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { type ClineAsk, type McpServerUse, isNonBlockingAsk } from "@roo-code/types"
1+
import { type ClineAsk, type McpServerUse, type FollowUpData, isNonBlockingAsk } from "@roo-code/types"
22

33
import type { ClineSayTool, ExtensionState } from "../../shared/ExtensionMessage"
4+
import { ClineAskResponse } from "../../shared/WebviewMessage"
45

56
import { isWriteToolAction, isReadOnlyToolAction } from "./tools"
67
import { isMcpToolAlwaysAllowed } from "./mcp"
@@ -25,77 +26,106 @@ export type AutoApprovalStateOptions =
2526
| "alwaysAllowReadOnlyOutsideWorkspace" // For `alwaysAllowReadOnly`.
2627
| "alwaysAllowWriteOutsideWorkspace" // For `alwaysAllowWrite`.
2728
| "alwaysAllowWriteProtected"
29+
| "followupAutoApproveTimeoutMs" // For `alwaysAllowFollowupQuestions`.
2830
| "mcpServers" // For `alwaysAllowMcp`.
2931
| "allowedCommands" // For `alwaysAllowExecute`.
3032
| "deniedCommands"
3133

32-
export type CheckAutoApprovalResult = "approve" | "deny" | "ask"
34+
export type CheckAutoApprovalResult =
35+
| { decision: "approve" }
36+
| { decision: "deny" }
37+
| { decision: "ask" }
38+
| {
39+
decision: "timeout"
40+
timeout: number
41+
fn: () => { askResponse: ClineAskResponse; text?: string; images?: string[] }
42+
}
3343

3444
export async function checkAutoApproval({
3545
state,
3646
ask,
3747
text,
3848
isProtected,
3949
}: {
40-
state: Pick<ExtensionState, AutoApprovalState | AutoApprovalStateOptions>
50+
state?: Pick<ExtensionState, AutoApprovalState | AutoApprovalStateOptions>
4151
ask: ClineAsk
4252
text?: string
4353
isProtected?: boolean
4454
}): Promise<CheckAutoApprovalResult> {
4555
if (isNonBlockingAsk(ask)) {
46-
return "approve"
56+
return { decision: "approve" }
4757
}
4858

49-
if (!state.autoApprovalEnabled) {
50-
return "ask"
59+
if (!state || !state.autoApprovalEnabled) {
60+
return { decision: "ask" }
5161
}
5262

53-
// Note: The `alwaysApproveResubmit` check is already handled in `Task`.
54-
5563
if (ask === "followup") {
56-
return state.alwaysAllowFollowupQuestions === true ? "approve" : "ask"
64+
if (state.alwaysAllowFollowupQuestions === true) {
65+
try {
66+
const suggestion = (JSON.parse(text || "{}") as FollowUpData).suggest?.[0]
67+
68+
if (
69+
suggestion &&
70+
typeof state.followupAutoApproveTimeoutMs === "number" &&
71+
state.followupAutoApproveTimeoutMs > 0
72+
) {
73+
return {
74+
decision: "timeout",
75+
timeout: state.followupAutoApproveTimeoutMs,
76+
fn: () => ({ askResponse: "messageResponse", text: suggestion.answer }),
77+
}
78+
} else {
79+
return { decision: "ask" }
80+
}
81+
} catch (error) {
82+
return { decision: "ask" }
83+
}
84+
} else {
85+
return { decision: "ask" }
86+
}
5787
}
5888

5989
if (ask === "browser_action_launch") {
60-
return state.alwaysAllowBrowser === true ? "approve" : "ask"
90+
return state.alwaysAllowBrowser === true ? { decision: "approve" } : { decision: "ask" }
6191
}
6292

6393
if (ask === "use_mcp_server") {
6494
if (!text) {
65-
return "ask"
95+
return { decision: "ask" }
6696
}
6797

6898
try {
6999
const mcpServerUse = JSON.parse(text) as McpServerUse
70100

71101
if (mcpServerUse.type === "use_mcp_tool") {
72102
return state.alwaysAllowMcp === true && isMcpToolAlwaysAllowed(mcpServerUse, state.mcpServers)
73-
? "approve"
74-
: "ask"
103+
? { decision: "approve" }
104+
: { decision: "ask" }
75105
} else if (mcpServerUse.type === "access_mcp_resource") {
76-
return state.alwaysAllowMcp === true ? "approve" : "ask"
106+
return state.alwaysAllowMcp === true ? { decision: "approve" } : { decision: "ask" }
77107
}
78108
} catch (error) {
79-
return "ask"
109+
return { decision: "ask" }
80110
}
81111

82-
return "ask"
112+
return { decision: "ask" }
83113
}
84114

85115
if (ask === "command") {
86116
if (!text) {
87-
return "ask"
117+
return { decision: "ask" }
88118
}
89119

90120
if (state.alwaysAllowExecute === true) {
91121
const decision = getCommandDecision(text, state.allowedCommands || [], state.deniedCommands || [])
92122

93123
if (decision === "auto_approve") {
94-
return "approve"
124+
return { decision: "approve" }
95125
} else if (decision === "auto_deny") {
96-
return "deny"
126+
return { decision: "deny" }
97127
} else {
98-
return "ask"
128+
return { decision: "ask" }
99129
}
100130
}
101131
}
@@ -110,50 +140,50 @@ export async function checkAutoApproval({
110140
}
111141

112142
if (!tool) {
113-
return "ask"
143+
return { decision: "ask" }
114144
}
115145

116146
if (tool.tool === "updateTodoList") {
117-
return state.alwaysAllowUpdateTodoList === true ? "approve" : "ask"
147+
return state.alwaysAllowUpdateTodoList === true ? { decision: "approve" } : { decision: "ask" }
118148
}
119149

120150
if (tool?.tool === "fetchInstructions") {
121151
if (tool.content === "create_mode") {
122-
return state.alwaysAllowModeSwitch === true ? "approve" : "ask"
152+
return state.alwaysAllowModeSwitch === true ? { decision: "approve" } : { decision: "ask" }
123153
}
124154

125155
if (tool.content === "create_mcp_server") {
126-
return state.alwaysAllowMcp === true ? "approve" : "ask"
156+
return state.alwaysAllowMcp === true ? { decision: "approve" } : { decision: "ask" }
127157
}
128158
}
129159

130160
if (tool?.tool === "switchMode") {
131-
return state.alwaysAllowModeSwitch === true ? "approve" : "ask"
161+
return state.alwaysAllowModeSwitch === true ? { decision: "approve" } : { decision: "ask" }
132162
}
133163

134164
if (["newTask", "finishTask"].includes(tool?.tool)) {
135-
return state.alwaysAllowSubtasks === true ? "approve" : "ask"
165+
return state.alwaysAllowSubtasks === true ? { decision: "approve" } : { decision: "ask" }
136166
}
137167

138168
const isOutsideWorkspace = !!tool.isOutsideWorkspace
139169

140170
if (isReadOnlyToolAction(tool)) {
141171
return state.alwaysAllowReadOnly === true &&
142172
(!isOutsideWorkspace || state.alwaysAllowReadOnlyOutsideWorkspace === true)
143-
? "approve"
144-
: "ask"
173+
? { decision: "approve" }
174+
: { decision: "ask" }
145175
}
146176

147177
if (isWriteToolAction(tool)) {
148178
return state.alwaysAllowWrite === true &&
149179
(!isOutsideWorkspace || state.alwaysAllowWriteOutsideWorkspace === true) &&
150180
(!isProtected || state.alwaysAllowWriteProtected === true)
151-
? "approve"
152-
: "ask"
181+
? { decision: "approve" }
182+
: { decision: "ask" }
153183
}
154184
}
155185

156-
return "ask"
186+
return { decision: "ask" }
157187
}
158188

159189
export { AutoApprovalHandler } from "./AutoApprovalHandler"

src/core/task/Task.ts

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -823,46 +823,55 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
823823
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, isProtected })
824824
}
825825

826+
let timeouts: NodeJS.Timeout[] = []
827+
826828
// Automatically approve if the ask according to the user's settings.
827829
const provider = this.providerRef.deref()
828830
const state = provider ? await provider.getState() : undefined
829-
const approval = state ? await checkAutoApproval({ state, ask: type, text, isProtected }) : false
831+
const approval = await checkAutoApproval({ state, ask: type, text, isProtected })
830832

831-
if (approval === "approve") {
833+
if (approval.decision === "approve") {
832834
this.approveAsk()
833-
} else if (approval === "deny") {
835+
} else if (approval.decision === "deny") {
834836
this.denyAsk()
837+
} else if (approval.decision === "timeout") {
838+
timeouts.push(
839+
setTimeout(() => {
840+
const { askResponse, text, images } = approval.fn()
841+
this.handleWebviewAskResponse(askResponse, text, images)
842+
}, approval.timeout),
843+
)
835844
}
836845

837846
// The state is mutable if the message is complete and the task will
838847
// block (via the `pWaitFor`).
839848
const isBlocking = !(this.askResponse !== undefined || this.lastMessageTs !== askTs)
840849
const isMessageQueued = !this.messageQueueService.isEmpty()
841850

842-
const isStatusMutable = !partial && isBlocking && !isMessageQueued && approval === "ask"
843-
let statusMutationTimeouts: NodeJS.Timeout[] = []
844-
const statusMutationTimeout = 5_000
851+
const isStatusMutable = !partial && isBlocking && !isMessageQueued && approval.decision === "ask"
845852

846853
if (isBlocking) {
847854
console.log(`Task#ask will block -> type: ${type}`)
848855
}
849856

850857
if (isStatusMutable) {
851858
console.log(`Task#ask: status is mutable -> type: ${type}`)
859+
const statusMutationTimeout = 2_000
852860

853861
if (isInteractiveAsk(type)) {
854-
statusMutationTimeouts.push(
862+
timeouts.push(
855863
setTimeout(() => {
856864
const message = this.findMessageByTimestamp(askTs)
857865

858866
if (message) {
859867
this.interactiveAsk = message
860868
this.emit(RooCodeEventName.TaskInteractive, this.taskId)
869+
provider?.postMessageToWebview({ type: "interactionRequired" })
861870
}
862871
}, statusMutationTimeout),
863872
)
864873
} else if (isResumableAsk(type)) {
865-
statusMutationTimeouts.push(
874+
timeouts.push(
866875
setTimeout(() => {
867876
const message = this.findMessageByTimestamp(askTs)
868877

@@ -873,7 +882,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
873882
}, statusMutationTimeout),
874883
)
875884
} else if (isIdleAsk(type)) {
876-
statusMutationTimeouts.push(
885+
timeouts.push(
877886
setTimeout(() => {
878887
const message = this.findMessageByTimestamp(askTs)
879888

@@ -925,7 +934,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
925934
this.askResponseImages = undefined
926935

927936
// Cancel the timeouts if they are still running.
928-
statusMutationTimeouts.forEach((timeout) => clearTimeout(timeout))
937+
timeouts.forEach((timeout) => clearTimeout(timeout))
929938

930939
// Switch back to an active state.
931940
if (this.idleAsk || this.resumableAsk || this.interactiveAsk) {

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ export interface ExtensionMessage {
127127
| "insertTextIntoTextarea"
128128
| "dismissedUpsells"
129129
| "organizationSwitchResult"
130+
| "interactionRequired"
130131
text?: string
131132
payload?: any // Add a generic payload for now, can refine later
132133
// Checkpoint warning message

0 commit comments

Comments
 (0)