Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ export const globalSettingsSchema = z.object({
alwaysAllowModeSwitch: z.boolean().optional(),
alwaysAllowSubtasks: z.boolean().optional(),
alwaysAllowExecute: z.boolean().optional(),
alwaysAllowFollowupQuestions: z.boolean().optional(),
followupAutoApproveTimeoutMs: z.number().optional(),
allowedCommands: z.array(z.string()).optional(),
allowedMaxRequests: z.number().nullish(),
autoCondenseContext: z.boolean().optional(),
Expand Down Expand Up @@ -189,6 +191,8 @@ export const EVALS_SETTINGS: RooCodeSettings = {
alwaysAllowModeSwitch: true,
alwaysAllowSubtasks: true,
alwaysAllowExecute: true,
alwaysAllowFollowupQuestions: true,
followupAutoApproveTimeoutMs: 0,
allowedCommands: ["*"],

browserToolEnabled: false,
Expand Down
6 changes: 6 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,8 @@ export class ClineProvider
codebaseIndexConfig,
codebaseIndexModels,
profileThresholds,
alwaysAllowFollowupQuestions,
followupAutoApproveTimeoutMs,
} = await this.getState()

const telemetryKey = process.env.POSTHOG_API_KEY
Expand Down Expand Up @@ -1521,6 +1523,8 @@ export class ClineProvider
profileThresholds: profileThresholds ?? {},
cloudApiUrl: getRooCodeApiUrl(),
hasOpenedModeSelector: this.getGlobalState("hasOpenedModeSelector") ?? false,
alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions ?? false,
followupAutoApproveTimeoutMs: followupAutoApproveTimeoutMs ?? 10000,
}
}

Expand Down Expand Up @@ -1601,6 +1605,8 @@ export class ClineProvider
alwaysAllowMcp: stateValues.alwaysAllowMcp ?? false,
alwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false,
alwaysAllowSubtasks: stateValues.alwaysAllowSubtasks ?? false,
alwaysAllowFollowupQuestions: stateValues.alwaysAllowFollowupQuestions ?? false,
followupAutoApproveTimeoutMs: stateValues.followupAutoApproveTimeoutMs ?? 10000,
allowedMaxRequests: stateValues.allowedMaxRequests,
autoCondenseContext: stateValues.autoCondenseContext ?? true,
autoCondenseContextPercent: stateValues.autoCondenseContextPercent ?? 100,
Expand Down
8 changes: 8 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,14 @@ export const webviewMessageHandler = async (
await updateGlobalState("maxWorkspaceFiles", fileCount)
await provider.postStateToWebview()
break
case "alwaysAllowFollowupQuestions":
await updateGlobalState("alwaysAllowFollowupQuestions", message.bool ?? false)
await provider.postStateToWebview()
break
case "followupAutoApproveTimeoutMs":
await updateGlobalState("followupAutoApproveTimeoutMs", message.value)
await provider.postStateToWebview()
break
case "browserToolEnabled":
await updateGlobalState("browserToolEnabled", message.bool ?? true)
await provider.postStateToWebview()
Expand Down
2 changes: 2 additions & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export interface WebviewMessage {
| "alwaysAllowWriteOutsideWorkspace"
| "alwaysAllowWriteProtected"
| "alwaysAllowExecute"
| "alwaysAllowFollowupQuestions"
| "followupAutoApproveTimeoutMs"
| "webviewDidLaunch"
| "newTask"
| "askResponse"
Expand Down
8 changes: 8 additions & 0 deletions webview-ui/src/components/chat/AutoApproveMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
alwaysAllowModeSwitch,
alwaysAllowSubtasks,
alwaysApproveResubmit,
alwaysAllowFollowupQuestions,
allowedMaxRequests,
setAlwaysAllowReadOnly,
setAlwaysAllowWrite,
Expand All @@ -34,6 +35,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
setAlwaysAllowModeSwitch,
setAlwaysAllowSubtasks,
setAlwaysApproveResubmit,
setAlwaysAllowFollowupQuestions,
setAllowedMaxRequests,
} = useExtensionState()

Expand Down Expand Up @@ -68,6 +70,9 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
case "alwaysApproveResubmit":
setAlwaysApproveResubmit(value)
break
case "alwaysAllowFollowupQuestions":
setAlwaysAllowFollowupQuestions(value)
break
}
},
[
Expand All @@ -79,6 +84,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
setAlwaysAllowModeSwitch,
setAlwaysAllowSubtasks,
setAlwaysApproveResubmit,
setAlwaysAllowFollowupQuestions,
],
)

Expand All @@ -94,6 +100,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
alwaysAllowModeSwitch: alwaysAllowModeSwitch,
alwaysAllowSubtasks: alwaysAllowSubtasks,
alwaysApproveResubmit: alwaysApproveResubmit,
alwaysAllowFollowupQuestions: alwaysAllowFollowupQuestions,
}),
[
alwaysAllowReadOnly,
Expand All @@ -104,6 +111,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => {
alwaysAllowModeSwitch,
alwaysAllowSubtasks,
alwaysApproveResubmit,
alwaysAllowFollowupQuestions,
],
)

Expand Down
40 changes: 33 additions & 7 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
alwaysAllowMcp,
allowedCommands,
writeDelayMs,
followupAutoApproveTimeoutMs,
mode,
setMode,
autoApprovalEnabled,
alwaysAllowModeSwitch,
alwaysAllowSubtasks,
alwaysAllowFollowupQuestions,
customModes,
telemetrySetting,
hasSystemPromptOverride,
Expand Down Expand Up @@ -879,6 +881,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
return false
}

if (message.ask === "followup") {
return alwaysAllowFollowupQuestions
}

if (message.ask === "browser_action_launch") {
return alwaysAllowBrowser
}
Expand Down Expand Up @@ -957,6 +963,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
alwaysAllowMcp,
isMcpToolAlwaysAllowed,
alwaysAllowModeSwitch,
alwaysAllowFollowupQuestions,
alwaysAllowSubtasks,
],
)
Expand Down Expand Up @@ -1270,19 +1277,35 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro

const autoApprove = async () => {
if (lastMessage?.ask && isAutoApproved(lastMessage)) {
if (lastMessage.ask === "tool" && isWriteToolAction(lastMessage)) {
// Special handling for follow-up questions
if (lastMessage.ask === "followup") {
const followUpData = JSON.parse(lastMessage.text || "{}")
if (followUpData && followUpData.suggest && followUpData.suggest.length > 0) {
// Wait for the configured timeout before auto-selecting the first suggestion
await new Promise<void>((resolve) => {
autoApproveTimeoutRef.current = setTimeout(resolve, followupAutoApproveTimeoutMs)
})

// Get the first suggestion
const firstSuggestion = followUpData.suggest[0]
const suggestionText =
typeof firstSuggestion === "string" ? firstSuggestion : firstSuggestion.answer

// Handle the suggestion click
handleSuggestionClickInRow(suggestionText)
return
}
} else if (lastMessage.ask === "tool" && isWriteToolAction(lastMessage)) {
await new Promise<void>((resolve) => {
autoApproveTimeoutRef.current = setTimeout(resolve, writeDelayMs)
})
}

if (autoApproveTimeoutRef.current === null || autoApproveTimeoutRef.current) {
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonClicked" })

setSendingDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
}
setSendingDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
}
}
autoApprove()
Expand All @@ -1303,6 +1326,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
alwaysAllowWrite,
alwaysAllowWriteOutsideWorkspace,
alwaysAllowExecute,
followupAutoApproveTimeoutMs,
alwaysAllowMcp,
messages,
allowedCommands,
Expand All @@ -1311,6 +1335,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
lastMessage,
writeDelayMs,
isWriteToolAction,
alwaysAllowFollowupQuestions,
handleSuggestionClickInRow,
])

// Function to handle mode switching
Expand Down
58 changes: 56 additions & 2 deletions webview-ui/src/components/chat/FollowUpSuggest.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { useCallback } from "react"
import { useCallback, useEffect, useState } from "react"
import { Edit } from "lucide-react"

import { Button, StandardTooltip } from "@/components/ui"
import { vscode } from "@/utils/vscode"

import { useAppTranslation } from "@src/i18n/TranslationContext"
import { useExtensionState } from "@src/context/ExtensionStateContext"

interface SuggestionItem {
answer: string
Expand All @@ -18,7 +19,47 @@ interface FollowUpSuggestProps {
}

export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1 }: FollowUpSuggestProps) => {
const { autoApprovalEnabled, alwaysAllowFollowupQuestions, followupAutoApproveTimeoutMs } = useExtensionState()
const [countdown, setCountdown] = useState<number | null>(null)
const [suggestionSelected, setSuggestionSelected] = useState(false)
const { t } = useAppTranslation()

// Start countdown timer when auto-approval is enabled for follow-up questions
useEffect(() => {
// Only start countdown if auto-approval is enabled for follow-up questions and no suggestion has been selected
if (autoApprovalEnabled && alwaysAllowFollowupQuestions && suggestions.length > 0 && !suggestionSelected) {
// Start with the configured timeout in seconds
const timeoutMs =
typeof followupAutoApproveTimeoutMs === "number" && !isNaN(followupAutoApproveTimeoutMs)
? followupAutoApproveTimeoutMs
: 10000

// Convert milliseconds to seconds for the countdown
setCountdown(Math.floor(timeoutMs / 1000))

// Update countdown every second
const intervalId = setInterval(() => {
setCountdown((prevCountdown) => {
if (prevCountdown === null || prevCountdown <= 1) {
clearInterval(intervalId)
return null
}
return prevCountdown - 1
})
}, 1000)

// Clean up interval on unmount
return () => clearInterval(intervalId)
} else {
setCountdown(null)
}
}, [
autoApprovalEnabled,
alwaysAllowFollowupQuestions,
suggestions,
followupAutoApproveTimeoutMs,
suggestionSelected,
])
const handleSuggestionClick = useCallback(
(suggestion: string | SuggestionItem, event: React.MouseEvent) => {
const suggestionText = typeof suggestion === "string" ? suggestion : suggestion.answer
Expand All @@ -32,6 +73,11 @@ export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1 }:
})
}

// Mark a suggestion as selected if it's not a shift-click (which just copies to input)
if (!event.shiftKey) {
setSuggestionSelected(true)
}

onSuggestionClick?.(suggestionText, event)
},
[onSuggestionClick],
Expand All @@ -44,9 +90,10 @@ export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1 }:

return (
<div className="flex mb-2 flex-col h-full gap-2">
{suggestions.map((suggestion) => {
{suggestions.map((suggestion, index) => {
const suggestionText = typeof suggestion === "string" ? suggestion : suggestion.answer
const mode = typeof suggestion === "object" ? suggestion.mode : undefined
const isFirstSuggestion = index === 0

return (
<div key={`${suggestionText}-${ts}`} className="w-full relative group">
Expand All @@ -56,6 +103,13 @@ export const FollowUpSuggest = ({ suggestions = [], onSuggestionClick, ts = 1 }:
onClick={(event) => handleSuggestionClick(suggestion, event)}
aria-label={suggestionText}>
{suggestionText}
{isFirstSuggestion && countdown !== null && !suggestionSelected && (
<span
className="ml-2 px-1.5 py-0.5 text-xs rounded-full bg-vscode-badge-background text-vscode-badge-foreground"
title={t("chat:followUpSuggest.autoSelectCountdown", { count: countdown })}>
{countdown}s
</span>
)}
</Button>
{mode && (
<div className="absolute bottom-0 right-0 text-[10px] bg-vscode-badge-background text-vscode-badge-foreground px-1 py-0.5 border border-vscode-badge-background flex items-center gap-0.5">
Expand Down
34 changes: 34 additions & 0 deletions webview-ui/src/components/settings/AutoApproveSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
alwaysAllowModeSwitch?: boolean
alwaysAllowSubtasks?: boolean
alwaysAllowExecute?: boolean
alwaysAllowFollowupQuestions?: boolean
followupAutoApproveTimeoutMs?: number
allowedCommands?: string[]
setCachedStateField: SetCachedStateField<
| "alwaysAllowReadOnly"
Expand All @@ -40,6 +42,8 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
| "alwaysAllowModeSwitch"
| "alwaysAllowSubtasks"
| "alwaysAllowExecute"
| "alwaysAllowFollowupQuestions"
| "followupAutoApproveTimeoutMs"
| "allowedCommands"
>
}
Expand All @@ -58,6 +62,8 @@ export const AutoApproveSettings = ({
alwaysAllowModeSwitch,
alwaysAllowSubtasks,
alwaysAllowExecute,
alwaysAllowFollowupQuestions,
followupAutoApproveTimeoutMs = 10000,
allowedCommands,
setCachedStateField,
...props
Expand Down Expand Up @@ -95,6 +101,7 @@ export const AutoApproveSettings = ({
alwaysAllowModeSwitch={alwaysAllowModeSwitch}
alwaysAllowSubtasks={alwaysAllowSubtasks}
alwaysAllowExecute={alwaysAllowExecute}
alwaysAllowFollowupQuestions={alwaysAllowFollowupQuestions}
onToggle={(key, value) => setCachedStateField(key, value)}
/>

Expand Down Expand Up @@ -202,6 +209,33 @@ export const AutoApproveSettings = ({
</div>
)}

{alwaysAllowFollowupQuestions && (
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div className="flex items-center gap-4 font-bold">
<span className="codicon codicon-question" />
<div>{t("settings:autoApprove.followupQuestions.label")}</div>
</div>
<div>
<div className="flex items-center gap-2">
<Slider
min={1000}
max={30000}
step={1000}
value={[followupAutoApproveTimeoutMs]}
onValueChange={([value]) =>
setCachedStateField("followupAutoApproveTimeoutMs", value)
}
data-testid="followup-timeout-slider"
/>
<span className="w-20">{followupAutoApproveTimeoutMs / 1000}s</span>
</div>
<div className="text-vscode-descriptionForeground text-sm mt-1">
{t("settings:autoApprove.followupQuestions.timeoutLabel")}
</div>
</div>
</div>
)}

{alwaysAllowExecute && (
<div className="flex flex-col gap-3 pl-3 border-l-2 border-vscode-button-background">
<div className="flex items-center gap-4 font-bold">
Expand Down
Loading