mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-05 08:10:14 +00:00
feat: add custom denial messages for commands
- Update type definitions to support both string and object format for denied commands - Add UI for entering custom messages when denying commands - Use custom messages in ChatView when auto-denying commands - Add translations for new UI elements This allows users to provide specific guidance when denying commands, such as suggesting 'uv run python' when denying direct 'python' commands. Fixes #8703
This commit is contained in:
parent
3aa9762d04
commit
15f0ecd1fa
6 changed files with 131 additions and 54 deletions
|
|
@ -68,7 +68,17 @@ export const globalSettingsSchema = z.object({
|
|||
followupAutoApproveTimeoutMs: z.number().optional(),
|
||||
alwaysAllowUpdateTodoList: z.boolean().optional(),
|
||||
allowedCommands: z.array(z.string()).optional(),
|
||||
deniedCommands: z.array(z.string()).optional(),
|
||||
deniedCommands: z
|
||||
.union([
|
||||
z.array(z.string()),
|
||||
z.array(
|
||||
z.object({
|
||||
command: z.string(),
|
||||
message: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
])
|
||||
.optional(),
|
||||
commandExecutionTimeout: z.number().optional(),
|
||||
commandTimeoutAllowlist: z.array(z.string()).optional(),
|
||||
preventCompletionWithOpenTodos: z.boolean().optional(),
|
||||
|
|
|
|||
|
|
@ -241,7 +241,7 @@ export interface WebviewMessage {
|
|||
images?: string[]
|
||||
bool?: boolean
|
||||
value?: number
|
||||
commands?: string[]
|
||||
commands?: string[] | { command: string; message?: string }[]
|
||||
audioType?: AudioType
|
||||
serverName?: string
|
||||
toolName?: string
|
||||
|
|
|
|||
|
|
@ -26,12 +26,7 @@ import { ProfileValidator } from "@roo/ProfileValidator"
|
|||
import { getLatestTodo } from "@roo/todo"
|
||||
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
import {
|
||||
getCommandDecision,
|
||||
CommandDecision,
|
||||
findLongestPrefixMatch,
|
||||
parseCommand,
|
||||
} from "@src/utils/command-validation"
|
||||
import { getCommandDecision, CommandDecision, parseCommand } from "@src/utils/command-validation"
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
||||
import { useSelectedModel } from "@src/components/ui/hooks/useSelectedModel"
|
||||
|
|
@ -1028,7 +1023,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
const getCommandDecisionForMessage = useCallback(
|
||||
(message: ClineMessage | undefined): CommandDecision => {
|
||||
if (message?.type !== "ask") return "ask_user"
|
||||
return getCommandDecision(message.text || "", allowedCommands || [], deniedCommands || [])
|
||||
// Convert deniedCommands to string array for getCommandDecision
|
||||
const deniedCommandsArray =
|
||||
deniedCommands?.map((cmd) => (typeof cmd === "string" ? cmd : cmd.command)) || []
|
||||
return getCommandDecision(message.text || "", allowedCommands || [], deniedCommandsArray)
|
||||
},
|
||||
[allowedCommands, deniedCommands],
|
||||
)
|
||||
|
|
@ -1049,17 +1047,32 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
[getCommandDecisionForMessage],
|
||||
)
|
||||
|
||||
// Helper function to get the denied prefix for a command
|
||||
const getDeniedPrefix = useCallback(
|
||||
(command: string): string | null => {
|
||||
// Helper function to get the denied command and its custom message
|
||||
const getDeniedCommand = useCallback(
|
||||
(command: string): { prefix: string; message?: string } | null => {
|
||||
if (!command || !deniedCommands?.length) return null
|
||||
|
||||
// Normalize deniedCommands to objects
|
||||
const normalizedDenied = deniedCommands.map((cmd) => (typeof cmd === "string" ? { command: cmd } : cmd))
|
||||
|
||||
// Parse the command into sub-commands and check each one
|
||||
const subCommands = parseCommand(command)
|
||||
for (const cmd of subCommands) {
|
||||
const deniedMatch = findLongestPrefixMatch(cmd, deniedCommands)
|
||||
if (deniedMatch) {
|
||||
return deniedMatch
|
||||
// Find longest matching denied command
|
||||
let longestMatch: { command: string; message?: string } | null = null
|
||||
let longestLength = 0
|
||||
|
||||
for (const denied of normalizedDenied) {
|
||||
if (cmd.toLowerCase().startsWith(denied.command.toLowerCase())) {
|
||||
if (denied.command.length > longestLength) {
|
||||
longestMatch = denied
|
||||
longestLength = denied.command.length
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (longestMatch) {
|
||||
return { prefix: longestMatch.command, message: longestMatch.message }
|
||||
}
|
||||
}
|
||||
return null
|
||||
|
|
@ -1582,11 +1595,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
const autoApproveOrReject = async () => {
|
||||
// Check for auto-reject first (commands that should be denied)
|
||||
if (lastMessage?.ask === "command" && isDeniedCommand(lastMessage)) {
|
||||
// Get the denied prefix for the localized message
|
||||
const deniedPrefix = getDeniedPrefix(lastMessage.text || "")
|
||||
if (deniedPrefix) {
|
||||
// Create the localized auto-deny message and send it with the rejection
|
||||
const autoDenyMessage = tSettings("autoApprove.execute.autoDenied", { prefix: deniedPrefix })
|
||||
// Get the denied command and its custom message
|
||||
const deniedCommand = getDeniedCommand(lastMessage.text || "")
|
||||
if (deniedCommand) {
|
||||
// Use custom message if provided, otherwise use default localized message
|
||||
const autoDenyMessage =
|
||||
deniedCommand.message ||
|
||||
tSettings("autoApprove.execute.autoDenied", { prefix: deniedCommand.prefix })
|
||||
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
|
|
@ -1686,7 +1701,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
handleSuggestionClickInRow,
|
||||
isAllowedCommand,
|
||||
isDeniedCommand,
|
||||
getDeniedPrefix,
|
||||
getDeniedCommand,
|
||||
tSettings,
|
||||
])
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
|
|||
allowedCommands?: string[]
|
||||
allowedMaxRequests?: number | undefined
|
||||
allowedMaxCost?: number | undefined
|
||||
deniedCommands?: string[]
|
||||
deniedCommands?: string[] | { command: string; message?: string }[]
|
||||
setCachedStateField: SetCachedStateField<
|
||||
| "alwaysAllowReadOnly"
|
||||
| "alwaysAllowReadOnlyOutsideWorkspace"
|
||||
|
|
@ -86,6 +86,7 @@ export const AutoApproveSettings = ({
|
|||
const { t } = useAppTranslation()
|
||||
const [commandInput, setCommandInput] = useState("")
|
||||
const [deniedCommandInput, setDeniedCommandInput] = useState("")
|
||||
const [deniedMessageInput, setDeniedMessageInput] = useState("")
|
||||
const { autoApprovalEnabled, setAutoApprovalEnabled } = useExtensionState()
|
||||
|
||||
const toggles = useAutoApprovalToggles()
|
||||
|
|
@ -106,10 +107,20 @@ export const AutoApproveSettings = ({
|
|||
const handleAddDeniedCommand = () => {
|
||||
const currentCommands = deniedCommands ?? []
|
||||
|
||||
if (deniedCommandInput && !currentCommands.includes(deniedCommandInput)) {
|
||||
const newCommands = [...currentCommands, deniedCommandInput]
|
||||
// Normalize to always work with objects
|
||||
const normalizedCommands = currentCommands.map((cmd) => (typeof cmd === "string" ? { command: cmd } : cmd))
|
||||
|
||||
// Check if command already exists
|
||||
const exists = normalizedCommands.some((item) => item.command === deniedCommandInput)
|
||||
|
||||
if (deniedCommandInput && !exists) {
|
||||
const newCommand = deniedMessageInput.trim()
|
||||
? { command: deniedCommandInput, message: deniedMessageInput.trim() }
|
||||
: { command: deniedCommandInput }
|
||||
const newCommands = [...normalizedCommands, newCommand]
|
||||
setCachedStateField("deniedCommands", newCommands)
|
||||
setDeniedCommandInput("")
|
||||
setDeniedMessageInput("")
|
||||
vscode.postMessage({ type: "deniedCommands", commands: newCommands })
|
||||
}
|
||||
}
|
||||
|
|
@ -361,45 +372,84 @@ export const AutoApproveSettings = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={deniedCommandInput}
|
||||
onChange={(e: any) => setDeniedCommandInput(e.target.value)}
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleAddDeniedCommand()
|
||||
}
|
||||
}}
|
||||
placeholder={t("settings:autoApprove.execute.deniedCommandPlaceholder")}
|
||||
className="grow"
|
||||
data-testid="denied-command-input"
|
||||
/>
|
||||
<Button
|
||||
className="h-8"
|
||||
onClick={handleAddDeniedCommand}
|
||||
data-testid="add-denied-command-button">
|
||||
{t("settings:autoApprove.execute.addButton")}
|
||||
</Button>
|
||||
</div>
|
||||
<Input
|
||||
value={deniedCommandInput}
|
||||
onChange={(e: any) => setDeniedCommandInput(e.target.value)}
|
||||
value={deniedMessageInput}
|
||||
onChange={(e: any) => setDeniedMessageInput(e.target.value)}
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleAddDeniedCommand()
|
||||
}
|
||||
}}
|
||||
placeholder={t("settings:autoApprove.execute.deniedCommandPlaceholder")}
|
||||
className="grow"
|
||||
data-testid="denied-command-input"
|
||||
placeholder={t("settings:autoApprove.execute.customMessagePlaceholder", {
|
||||
defaultValue: "Custom message (optional, e.g., 'Use uv run python instead')",
|
||||
})}
|
||||
className="w-full text-sm"
|
||||
data-testid="denied-message-input"
|
||||
/>
|
||||
<Button
|
||||
className="h-8"
|
||||
onClick={handleAddDeniedCommand}
|
||||
data-testid="add-denied-command-button">
|
||||
{t("settings:autoApprove.execute.addButton")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{(deniedCommands ?? []).map((cmd, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
variant="secondary"
|
||||
data-testid={`remove-denied-command-${index}`}
|
||||
onClick={() => {
|
||||
const newCommands = (deniedCommands ?? []).filter((_, i) => i !== index)
|
||||
setCachedStateField("deniedCommands", newCommands)
|
||||
vscode.postMessage({ type: "deniedCommands", commands: newCommands })
|
||||
}}>
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
<div>{cmd}</div>
|
||||
<X className="text-foreground scale-75" />
|
||||
<div className="flex flex-col gap-2">
|
||||
{(deniedCommands ?? []).map((cmd, index) => {
|
||||
const commandStr = typeof cmd === "string" ? cmd : cmd.command
|
||||
const messageStr = typeof cmd === "string" ? null : cmd.message
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
className="flex flex-col gap-1 p-2 rounded border border-vscode-panel-border bg-vscode-editor-background">
|
||||
<div className="flex items-center justify-between">
|
||||
<code className="text-sm">{commandStr}</code>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
data-testid={`remove-denied-command-${index}`}
|
||||
onClick={() => {
|
||||
const newCommands = (deniedCommands ?? []).filter(
|
||||
(_, i) => i !== index,
|
||||
)
|
||||
setCachedStateField("deniedCommands", newCommands)
|
||||
vscode.postMessage({
|
||||
type: "deniedCommands",
|
||||
commands: newCommands,
|
||||
})
|
||||
}}>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
{messageStr && (
|
||||
<div className="text-xs text-vscode-descriptionForeground mt-1">
|
||||
{t("settings:autoApprove.execute.customMessage", {
|
||||
defaultValue: "Custom message:",
|
||||
})}{" "}
|
||||
{messageStr}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Button>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setShowRooIgnoredFiles: (value: boolean) => void
|
||||
setShowAnnouncement: (value: boolean) => void
|
||||
setAllowedCommands: (value: string[]) => void
|
||||
setDeniedCommands: (value: string[]) => void
|
||||
setDeniedCommands: (value: string[] | { command: string; message?: string }[]) => void
|
||||
setAllowedMaxRequests: (value: number | undefined) => void
|
||||
setAllowedMaxCost: (value: number | undefined) => void
|
||||
setSoundEnabled: (value: boolean) => void
|
||||
|
|
|
|||
|
|
@ -200,6 +200,8 @@
|
|||
"deniedCommandsDescription": "Command prefixes that will be automatically denied without asking for approval. In case of conflicts with allowed commands, the longest prefix match takes precedence. Add * to deny all commands.",
|
||||
"commandPlaceholder": "Enter command prefix (e.g., 'git ')",
|
||||
"deniedCommandPlaceholder": "Enter command prefix to deny (e.g., 'rm -rf')",
|
||||
"customMessagePlaceholder": "Custom message (optional, e.g., 'Use uv run python instead')",
|
||||
"customMessage": "Custom message:",
|
||||
"addButton": "Add",
|
||||
"autoDenied": "Commands with the prefix `{{prefix}}` have been forbidden by the user. Do not bypass this restriction by running another command."
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue